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
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-08-30 12:09:12 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-08-30 12:09:12 +0300
commit4ac9f1b8eaef29daa484b27a3113505cfa6a6dcb (patch)
treebda090fb8cf7c97765ee891bd58f7f9ee4271301
parentabbedc2027e64b11b03be4639411f1943b81f7ce (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/main.js1
-rw-r--r--app/assets/javascripts/main_jh.js1
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue29
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue40
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue189
-rw-r--r--app/finders/packages/helm/package_files_finder.rb2
-rw-r--r--app/models/packages/package_file.rb4
-rw-r--r--app/models/user.rb6
-rw-r--r--app/presenters/project_presenter.rb18
-rw-r--r--app/views/users/show.html.haml7
-rw-r--r--app/workers/packages/debian/process_changes_worker.rb7
-rw-r--r--app/workers/packages/helm/extraction_worker.rb4
-rw-r--r--config/feature_flags/development/linear_user_membership_groups.yml (renamed from config/feature_flags/experiment/repo_integrations_link.yml)12
-rw-r--r--config/helpers/incremental_webpack_compiler.js131
-rw-r--r--config/helpers/incremental_webpack_compiler/compiler.js117
-rw-r--r--config/helpers/incremental_webpack_compiler/history.js176
-rw-r--r--config/helpers/incremental_webpack_compiler/index.js17
-rw-r--r--config/helpers/incremental_webpack_compiler/log.js3
-rw-r--r--config/routes/user.rb2
-rw-r--r--config/webpack.config.js25
-rw-r--r--doc/user/project/repository/gpg_signed_commits/index.md5
-rw-r--r--jest.config.base.js22
-rw-r--r--jest.config.integration.js4
-rw-r--r--lib/api/helm_packages.rb2
-rw-r--r--lib/gitlab/database/load_balancing/service_discovery.rb6
-rw-r--r--lib/gitlab/gon_helper.rb1
-rw-r--r--locale/gitlab.pot5
-rw-r--r--spec/features/users/show_spec.rb23
-rw-r--r--spec/finders/packages/helm/package_files_finder_spec.rb35
-rw-r--r--spec/frontend/tracking_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js15
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_title_spec.js61
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js169
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js2
-rw-r--r--spec/models/packages/package_file_spec.rb4
-rw-r--r--spec/models/user_spec.rb27
-rw-r--r--spec/presenters/project_presenter_spec.rb38
-rw-r--r--spec/requests/api/helm_packages_spec.rb8
-rw-r--r--spec/support/database/prevent_cross_joins.rb9
-rw-r--r--spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb9
-rw-r--r--spec/support_specs/database/prevent_cross_joins_spec.rb10
-rw-r--r--spec/workers/packages/helm/extraction_worker_spec.rb14
47 files changed, 670 insertions, 619 deletions
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 1aaefcaa13b..abd13a30156 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -37,6 +37,7 @@ import initBroadcastNotifications from './broadcast_notification';
import { initTopNav } from './nav';
import 'ee_else_ce/main_ee';
+import 'jh_else_ce/main_jh';
applyGitLabUIConfig();
diff --git a/app/assets/javascripts/main_jh.js b/app/assets/javascripts/main_jh.js
new file mode 100644
index 00000000000..13a6b8f3d3d
--- /dev/null
+++ b/app/assets/javascripts/main_jh.js
@@ -0,0 +1 @@
+// This is an empty file to satisfy jh_else_ce import for the JH main entry point
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 031472a7d20..831f3987041 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -258,6 +258,7 @@ export function mountSidebarLabels() {
allowScopedLabels: parseBoolean(el.dataset.allowScopedLabels),
initiallySelectedLabels: JSON.parse(el.dataset.selectedLabels),
variant: DropdownVariant.Sidebar,
+ canUpdate: parseBoolean(el.dataset.canEdit),
},
render: (createElement) => createElement(SidebarLabels),
});
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
index 6694e349b6e..b5c3ffabae0 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
@@ -60,7 +60,7 @@ export default {
},
},
methods: {
- ...mapActions(['toggleDropdownContentsCreateView', 'toggleDropdownContents']),
+ ...mapActions(['toggleDropdownContentsCreateView']),
},
};
</script>
@@ -83,7 +83,7 @@ export default {
size="small"
class="js-btn-back dropdown-header-button p-0"
icon="arrow-left"
- @click="toggleDropdownContentsCreateView"
+ @click.stop="toggleDropdownContentsCreateView"
/>
<span class="flex-grow-1">{{ dropdownTitle }}</span>
<gl-button
@@ -92,7 +92,7 @@ export default {
size="small"
class="dropdown-header-button gl-p-0!"
icon="close"
- @click="toggleDropdownContents"
+ @click="$emit('closeDropdown')"
/>
</div>
<component
@@ -103,7 +103,7 @@ export default {
:footer-create-label-title="footerCreateLabelTitle"
:footer-manage-label-title="footerManageLabelTitle"
@hideCreateView="toggleDropdownContentsCreateView"
- @closeDropdown="$emit('closeDropdown', $event)"
+ @setLabels="$emit('setLabels', $event)"
@toggleDropdownContentsCreateView="toggleDropdownContentsCreateView"
/>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
index 4651e7a1576..2e31b386fdd 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
@@ -1,8 +1,10 @@
<script>
import { GlTooltipDirective, GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
+import produce from 'immer';
import createFlash from '~/flash';
import { __ } from '~/locale';
import createLabelMutation from './graphql/create_label.mutation.graphql';
+import projectLabelsQuery from './graphql/project_labels.query.graphql';
const errorMessage = __('Error creating label.');
@@ -47,6 +49,25 @@ export default {
handleColorClick(color) {
this.selectedColor = this.getColorCode(color);
},
+ updateLabelsInCache(store, label) {
+ const sourceData = store.readQuery({
+ query: projectLabelsQuery,
+ variables: { fullPath: this.projectPath, searchTerm: '' },
+ });
+
+ const collator = new Intl.Collator('en');
+ const data = produce(sourceData, (draftData) => {
+ const { nodes } = draftData.workspace.labels;
+ nodes.push(label);
+ nodes.sort((a, b) => collator.compare(a.title, b.title));
+ });
+
+ store.writeQuery({
+ query: projectLabelsQuery,
+ variables: { fullPath: this.projectPath, searchTerm: '' },
+ data,
+ });
+ },
async createLabel() {
this.labelCreateInProgress = true;
try {
@@ -59,6 +80,14 @@ export default {
color: this.selectedColor,
projectPath: this.projectPath,
},
+ update: (
+ store,
+ {
+ data: {
+ labelCreate: { label },
+ },
+ },
+ ) => this.updateLabelsInCache(store, label),
});
if (labelCreate.errors.length) {
createFlash({ message: errorMessage });
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
index ffa37424c2c..224dbd3e29b 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
@@ -112,7 +112,7 @@ export default {
this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
},
beforeDestroy() {
- this.$emit('closeDropdown', this.localSelectedLabels);
+ this.$emit('setLabels', this.localSelectedLabels);
this.debouncedSearchKeyUpdate.cancel();
},
methods: {
@@ -166,7 +166,7 @@ export default {
this.updateSelectedLabels(this.visibleLabels[this.currentHighlightItem]);
this.searchKey = '';
} else if (e.keyCode === ESC_KEY_CODE) {
- this.$emit('closeDropdown', this.localSelectedLabels);
+ this.$emit('setLabels', this.localSelectedLabels);
}
if (e.keyCode !== ESC_KEY_CODE) {
@@ -180,7 +180,7 @@ export default {
handleLabelClick(label) {
this.updateSelectedLabels(label);
if (!this.allowMultiselect) {
- this.$emit('closeDropdown', this.localSelectedLabels);
+ this.$emit('setLabels', this.localSelectedLabels);
}
},
setSearchKey(value) {
@@ -240,7 +240,7 @@ export default {
<gl-link
class="gl-display-flex gl-flex-direction-row gl-w-full gl-overflow-break-word label-item"
data-testid="create-label-button"
- @click="$emit('toggleDropdownContentsCreateView')"
+ @click.stop="$emit('toggleDropdownContentsCreateView')"
>
{{ footerCreateLabelTitle }}
</gl-link>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue
deleted file mode 100644
index 46edfa1c42a..00000000000
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue
+++ /dev/null
@@ -1,40 +0,0 @@
-<script>
-import { GlButton, GlLoadingIcon } from '@gitlab/ui';
-import { mapState, mapActions } from 'vuex';
-
-export default {
- components: {
- GlButton,
- GlLoadingIcon,
- },
- props: {
- labelsSelectInProgress: {
- type: Boolean,
- required: true,
- },
- },
- computed: {
- ...mapState(['allowLabelEdit', 'labelsFetchInProgress']),
- },
- methods: {
- ...mapActions(['toggleDropdownContents']),
- },
-};
-</script>
-
-<template>
- <div class="title hide-collapsed gl-mb-3">
- {{ __('Labels') }}
- <template v-if="allowLabelEdit">
- <gl-loading-icon v-show="labelsSelectInProgress" size="sm" inline />
- <gl-button
- category="tertiary"
- size="small"
- class="float-right js-sidebar-dropdown-toggle gl-mr-n2"
- data-qa-selector="labels_edit_button"
- @click="toggleDropdownContents"
- >{{ __('Edit') }}</gl-button
- >
- </template>
- </div>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql
index 9aa4f5d165e..eb478645a03 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql
@@ -6,9 +6,7 @@ mutation createLabel($title: String!, $color: String, $projectPath: ID, $groupPa
id
color
description
- descriptionHtml
title
- textColor
}
errors
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
index 0499dfe468f..efe524996b2 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
@@ -1,14 +1,12 @@
<script>
-import $ from 'jquery';
import Vue from 'vue';
import Vuex, { mapState, mapActions, mapGetters } from 'vuex';
import { isInViewport } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
-
+import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import { DropdownVariant } from './constants';
import DropdownButton from './dropdown_button.vue';
import DropdownContents from './dropdown_contents.vue';
-import DropdownTitle from './dropdown_title.vue';
import DropdownValue from './dropdown_value.vue';
import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
import issueLabelsQuery from './graphql/issue_labels.query.graphql';
@@ -19,11 +17,11 @@ Vue.use(Vuex);
export default {
store: new Vuex.Store(labelsSelectModule()),
components: {
- DropdownTitle,
DropdownValue,
DropdownButton,
DropdownContents,
DropdownValueCollapsed,
+ SidebarEditableItem,
},
inject: ['iid', 'projectPath'],
props: {
@@ -139,15 +137,12 @@ export default {
},
},
computed: {
- ...mapState(['showDropdownButton', 'showDropdownContents']),
+ ...mapState(['showDropdownContents']),
...mapGetters([
'isDropdownVariantSidebar',
'isDropdownVariantStandalone',
'isDropdownVariantEmbedded',
]),
- dropdownButtonVisible() {
- return this.isDropdownVariantSidebar ? this.showDropdownButton : true;
- },
},
watch: {
selectedLabels(selectedLabels) {
@@ -182,99 +177,20 @@ export default {
footerCreateLabelTitle: this.footerCreateLabelTitle,
footerManageLabelTitle: this.footerManageLabelTitle,
});
-
- this.$store.subscribeAction({
- after: this.handleVuexActionDispatch,
- });
-
- document.addEventListener('mousedown', this.handleDocumentMousedown);
- document.addEventListener('click', this.handleDocumentClick);
- },
- beforeDestroy() {
- document.removeEventListener('mousedown', this.handleDocumentMousedown);
- document.removeEventListener('click', this.handleDocumentClick);
},
methods: {
- ...mapActions(['setInitialState', 'toggleDropdownContents']),
- /**
- * This method stores a mousedown event's target.
- * Required by the click listener because the click
- * event itself has no reference to this element.
- */
- handleDocumentMousedown({ target }) {
- this.mousedownTarget = target;
- },
- /**
- * This method listens for document-wide click event
- * and toggle dropdown if user clicks anywhere outside
- * the dropdown while dropdown is visible.
- */
- handleDocumentClick({ target }) {
- // We also perform the toggle exception check for the
- // last mousedown event's target to avoid hiding the
- // box when the mousedown happened inside the box and
- // only the mouseup did not.
- if (
- this.showDropdownContents &&
- !this.preventDropdownToggleOnClick(target) &&
- !this.preventDropdownToggleOnClick(this.mousedownTarget)
- ) {
- this.toggleDropdownContents();
- }
- },
- /**
- * This method checks whether a given click target
- * should prevent the dropdown from being toggled.
- */
- preventDropdownToggleOnClick(target) {
- // This approach of element detection is needed
- // as the dropdown wrapper is not using `GlDropdown` as
- // it will also require us to use `BDropdownForm`
- // which is yet to be implemented in GitLab UI.
- const hasExceptionClass = [
- 'js-dropdown-button',
- 'js-btn-cancel-create',
- 'js-sidebar-dropdown-toggle',
- ].some(
- (className) =>
- target?.classList.contains(className) ||
- target?.parentElement?.classList.contains(className),
- );
-
- const hasExceptionParent = ['.js-btn-back', '.js-labels-list'].some(
- (className) => $(target).parents(className).length,
- );
-
- const isInDropdownButtonCollapsed = this.$refs.dropdownButtonCollapsed?.$el.contains(target);
-
- const isInDropdownContents = this.$refs.dropdownContents?.$el.contains(target);
-
- return (
- hasExceptionClass ||
- hasExceptionParent ||
- isInDropdownButtonCollapsed ||
- isInDropdownContents
- );
- },
+ ...mapActions(['setInitialState']),
handleDropdownClose(labels) {
- // Only emit label updates if there are any labels to update
- // on UI.
- if (this.showDropdownContents) {
- this.toggleDropdownContents();
- }
if (labels.length) this.$emit('updateSelectedLabels', labels);
this.$emit('onDropdownClose');
},
+ collapseDropdown() {
+ this.$refs.editable.collapse();
+ },
handleCollapsedValueClick() {
this.$emit('toggleCollapse');
},
- setContentIsOnViewport(showDropdownContents) {
- if (!showDropdownContents) {
- this.contentIsOnViewport = true;
-
- return;
- }
-
+ setContentIsOnViewport() {
this.$nextTick(() => {
if (this.$refs.dropdownContents) {
this.contentIsOnViewport = isInViewport(this.$refs.dropdownContents.$el);
@@ -299,48 +215,55 @@ export default {
:labels="issueLabels"
@onValueClick="handleCollapsedValueClick"
/>
- <dropdown-title
- :allow-label-edit="allowLabelEdit"
- :labels-select-in-progress="labelsSelectInProgress"
- />
- <dropdown-value
- :disable-labels="labelsSelectInProgress"
- :selected-labels="issueLabels"
- :allow-label-remove="allowLabelRemove"
- :allow-scoped-labels="allowScopedLabels"
- :labels-filter-base-path="labelsFilterBasePath"
- :labels-filter-param="labelsFilterParam"
- @onLabelRemove="$emit('onLabelRemove', $event)"
+ <sidebar-editable-item
+ ref="editable"
+ :title="__('Labels')"
+ :loading="labelsSelectInProgress"
+ @open="setContentIsOnViewport"
+ @close="contentIsOnViewport = true"
>
- <slot></slot>
- </dropdown-value>
- <dropdown-button v-show="dropdownButtonVisible" class="gl-mt-2" />
- <dropdown-contents
- v-if="dropdownButtonVisible && showDropdownContents"
- ref="dropdownContents"
- :allow-multiselect="allowMultiselect"
- :labels-list-title="labelsListTitle"
- :footer-create-label-title="footerCreateLabelTitle"
- :footer-manage-label-title="footerManageLabelTitle"
- :render-on-top="!contentIsOnViewport"
- :labels-create-title="labelsCreateTitle"
- :selected-labels="selectedLabels"
- @closeDropdown="handleDropdownClose"
- />
- </template>
- <template v-if="isDropdownVariantStandalone || isDropdownVariantEmbedded">
- <dropdown-button v-show="dropdownButtonVisible" />
- <dropdown-contents
- v-if="dropdownButtonVisible && showDropdownContents"
- ref="dropdownContents"
- :allow-multiselect="allowMultiselect"
- :labels-list-title="labelsListTitle"
- :footer-create-label-title="footerCreateLabelTitle"
- :footer-manage-label-title="footerManageLabelTitle"
- :render-on-top="!contentIsOnViewport"
- :selected-labels="selectedLabels"
- @closeDropdown="handleDropdownClose"
- />
+ <template #collapsed>
+ <dropdown-value
+ :disable-labels="labelsSelectInProgress"
+ :selected-labels="issueLabels"
+ :allow-label-remove="allowLabelRemove"
+ :allow-scoped-labels="allowScopedLabels"
+ :labels-filter-base-path="labelsFilterBasePath"
+ :labels-filter-param="labelsFilterParam"
+ @onLabelRemove="$emit('onLabelRemove', $event)"
+ >
+ <slot></slot>
+ </dropdown-value>
+ </template>
+ <template #default="{ edit }">
+ <dropdown-value
+ :disable-labels="labelsSelectInProgress"
+ :selected-labels="issueLabels"
+ :allow-label-remove="allowLabelRemove"
+ :allow-scoped-labels="allowScopedLabels"
+ :labels-filter-base-path="labelsFilterBasePath"
+ :labels-filter-param="labelsFilterParam"
+ class="gl-mb-2"
+ @onLabelRemove="$emit('onLabelRemove', $event)"
+ >
+ <slot></slot>
+ </dropdown-value>
+ <dropdown-button />
+ <dropdown-contents
+ v-if="edit"
+ ref="dropdownContents"
+ :allow-multiselect="allowMultiselect"
+ :labels-list-title="labelsListTitle"
+ :footer-create-label-title="footerCreateLabelTitle"
+ :footer-manage-label-title="footerManageLabelTitle"
+ :render-on-top="!contentIsOnViewport"
+ :labels-create-title="labelsCreateTitle"
+ :selected-labels="selectedLabels"
+ @closeDropdown="collapseDropdown"
+ @setLabels="handleDropdownClose"
+ />
+ </template>
+ </sidebar-editable-item>
</template>
</div>
</template>
diff --git a/app/finders/packages/helm/package_files_finder.rb b/app/finders/packages/helm/package_files_finder.rb
index ba400b27554..c6504d09dce 100644
--- a/app/finders/packages/helm/package_files_finder.rb
+++ b/app/finders/packages/helm/package_files_finder.rb
@@ -6,6 +6,8 @@ module Packages
DEFAULT_PACKAGE_FILES_COUNT = 20
MAX_PACKAGE_FILES_COUNT = 1000
+ delegate :most_recent!, to: :execute
+
def initialize(project, channel, params = {})
@project = project
@channel = channel
diff --git a/app/models/packages/package_file.rb b/app/models/packages/package_file.rb
index 8aa19397086..807c6c409c7 100644
--- a/app/models/packages/package_file.rb
+++ b/app/models/packages/package_file.rb
@@ -77,6 +77,10 @@ class Packages::PackageFile < ApplicationRecord
.where(packages_conan_file_metadata: { conan_package_reference: conan_package_reference })
end
+ def self.most_recent!
+ recent.first!
+ end
+
mount_file_store_uploader Packages::PackageFileUploader
update_project_statistics project_statistics_name: :packages_size
diff --git a/app/models/user.rb b/app/models/user.rb
index 12f167d1385..3920e61c953 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1000,7 +1000,11 @@ class User < ApplicationRecord
# Returns the groups a user is a member of, either directly or through a parent group
def membership_groups
- Gitlab::ObjectHierarchy.new(groups).base_and_descendants
+ if Feature.enabled?(:linear_user_membership_groups, self, default_enabled: :yaml)
+ groups.self_and_descendants
+ else
+ Gitlab::ObjectHierarchy.new(groups).base_and_descendants
+ end
end
# Returns a relation of groups the user has access to, including their parent
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index 80a8ee5cb3c..066f4786cff 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -431,22 +431,10 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
private
def integrations_anchor_data
- experiment(:repo_integrations_link, project: project) do |e|
- e.exclude! unless can?(current_user, :admin_project, project)
-
- e.use {} # nil control
- e.try do
- label = statistic_icon('settings') + _('Configure Integrations')
- AnchorData.new(false, label, project_settings_integrations_path(project), nil, nil, nil, {
- 'track-event': 'click',
- 'track-experiment': e.name
- })
- end
-
- e.run # call run so the return value will be the AnchorData (or nil)
+ return unless can?(current_user, :admin_project, project)
- e.track(:view, value: project.id) # track an event for the view, with project id
- end
+ label = statistic_icon('settings') + _('Configure Integrations')
+ AnchorData.new(false, label, project_settings_integrations_path(project), nil, nil, nil)
end
def cicd_missing?
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 8818cd9e6c4..ec0b59785ca 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -26,6 +26,13 @@
= link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referer), class: link_classes + 'btn gl-button btn-default btn-icon',
title: s_('UserProfile|Report abuse'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= sprite_icon('error')
+ - verified_gpg_keys = @user.gpg_keys.select(&:verified?)
+ - if verified_gpg_keys.any?
+ = link_to user_gpg_keys_path,
+ class: link_classes + 'btn btn-default btn-md gl-button btn-icon has-tooltip',
+ title: n_('View public GPG key', 'View public GPG keys', verified_gpg_keys.length),
+ data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
+ = sprite_icon('key', css_class: 'gl-button-icon gl-icon')
- if can?(current_user, :read_user_profile, @user)
= link_to user_path(@user, rss_url_options), class: link_classes + 'btn gl-button btn-default btn-icon has-tooltip',
title: s_('UserProfile|Subscribe'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
diff --git a/app/workers/packages/debian/process_changes_worker.rb b/app/workers/packages/debian/process_changes_worker.rb
index 3ddeb858429..0d10e18cc01 100644
--- a/app/workers/packages/debian/process_changes_worker.rb
+++ b/app/workers/packages/debian/process_changes_worker.rb
@@ -22,12 +22,7 @@ module Packages
return unless package_file && user
::Packages::Debian::ProcessChangesService.new(package_file, user).execute
- rescue ArgumentError,
- Packages::Debian::ExtractChangesMetadataService::ExtractionError,
- Packages::Debian::ExtractDebMetadataService::CommandFailedError,
- Packages::Debian::ExtractMetadataService::ExtractionError,
- Packages::Debian::ParseDebian822Service::InvalidDebian822Error,
- ActiveRecord::RecordNotFound => e
+ rescue StandardError => e
Gitlab::ErrorTracking.log_exception(e, package_file_id: @package_file_id, user_id: @user_id)
package_file.destroy!
end
diff --git a/app/workers/packages/helm/extraction_worker.rb b/app/workers/packages/helm/extraction_worker.rb
index 1010a0833b1..0ba2d149f77 100644
--- a/app/workers/packages/helm/extraction_worker.rb
+++ b/app/workers/packages/helm/extraction_worker.rb
@@ -20,9 +20,7 @@ module Packages
::Packages::Helm::ProcessFileService.new(channel, package_file).execute
- rescue ::Packages::Helm::ExtractFileMetadataService::ExtractionError,
- ::Packages::Helm::ProcessFileService::ExtractionError,
- ::ActiveModel::ValidationError => e
+ rescue StandardError => e
Gitlab::ErrorTracking.log_exception(e, project_id: package_file.project_id)
package_file.package.update_column(:status, :error)
end
diff --git a/config/feature_flags/experiment/repo_integrations_link.yml b/config/feature_flags/development/linear_user_membership_groups.yml
index 943429a84e7..19bca849090 100644
--- a/config/feature_flags/experiment/repo_integrations_link.yml
+++ b/config/feature_flags/development/linear_user_membership_groups.yml
@@ -1,8 +1,8 @@
---
-name: repo_integrations_link
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54652/
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/285154
-milestone: '13.10'
-type: experiment
-group: group::adoption
+name: linear_user_membership_groups
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68842
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339432
+milestone: '14.3'
+type: development
+group: group::access
default_enabled: false
diff --git a/config/helpers/incremental_webpack_compiler.js b/config/helpers/incremental_webpack_compiler.js
deleted file mode 100644
index 5d4f9bd040d..00000000000
--- a/config/helpers/incremental_webpack_compiler.js
+++ /dev/null
@@ -1,131 +0,0 @@
-/* eslint-disable max-classes-per-file, no-underscore-dangle */
-const fs = require('fs');
-const path = require('path');
-
-const log = (msg, ...rest) => console.log(`IncrementalWebpackCompiler: ${msg}`, ...rest);
-
-// If we force a recompile immediately, the page reload doesn't seem to work.
-// Five seconds seem to work fine and the user can read the message
-const TIMEOUT = 5000;
-
-/* eslint-disable class-methods-use-this */
-class NoopCompiler {
- constructor() {
- this.enabled = false;
- }
-
- filterEntryPoints(entryPoints) {
- return entryPoints;
- }
-
- logStatus() {}
-
- setupMiddleware() {}
-}
-/* eslint-enable class-methods-use-this */
-
-class IncrementalWebpackCompiler {
- constructor(historyFilePath) {
- this.enabled = true;
- this.history = {};
- this.compiledEntryPoints = new Set([
- // Login page
- 'pages.sessions.new',
- // Explore page
- 'pages.root',
- ]);
- this.historyFilePath = historyFilePath;
- this._loadFromHistory();
- }
-
- filterEntryPoints(entrypoints) {
- return Object.fromEntries(
- Object.entries(entrypoints).map(([key, val]) => {
- if (this.compiledEntryPoints.has(key)) {
- return [key, val];
- }
- return [key, ['./webpack_non_compiled_placeholder.js']];
- }),
- );
- }
-
- logStatus(totalCount) {
- const current = this.compiledEntryPoints.size;
- log(`Currently compiling route entrypoints: ${current} of ${totalCount}`);
- }
-
- setupMiddleware(app, server) {
- app.use((req, res, next) => {
- const fileName = path.basename(req.url);
-
- /**
- * We are only interested in files that have a name like `pages.foo.bar.chunk.js`
- * because those are the ones corresponding to our entry points.
- *
- * This filters out hot update files that are for example named "pages.foo.bar.[hash].hot-update.js"
- */
- if (fileName.startsWith('pages.') && fileName.endsWith('.chunk.js')) {
- const chunk = fileName.replace(/\.chunk\.js$/, '');
-
- this._addToHistory(chunk);
-
- if (!this.compiledEntryPoints.has(chunk)) {
- log(`First time we are seeing ${chunk}. Adding to compilation.`);
-
- this.compiledEntryPoints.add(chunk);
-
- setTimeout(() => {
- server.middleware.invalidate(() => {
- if (server.sockets) {
- server.sockWrite(server.sockets, 'content-changed');
- }
- });
- }, TIMEOUT);
- }
- }
-
- next();
- });
- }
-
- // private methods
-
- _addToHistory(chunk) {
- if (!this.history[chunk]) {
- this.history[chunk] = { lastVisit: null, count: 0 };
- }
- this.history[chunk].lastVisit = Date.now();
- this.history[chunk].count += 1;
-
- try {
- fs.writeFileSync(this.historyFilePath, JSON.stringify(this.history), 'utf8');
- } catch (e) {
- log('Warning – Could not write to history', e.message);
- }
- }
-
- _loadFromHistory() {
- try {
- this.history = JSON.parse(fs.readFileSync(this.historyFilePath, 'utf8'));
- const entryPoints = Object.keys(this.history);
- log(`Successfully loaded history containing ${entryPoints.length} entry points`);
- /*
- TODO: Let's ask a few folks to give us their history file after a milestone of usage
- Then we can make smarter decisions on when to throw out rather than rendering everything
- Something like top 20/30/40 entries visited in the last 7/10/15 days might be sufficient
- */
- this.compiledEntryPoints = new Set([...this.compiledEntryPoints, ...entryPoints]);
- } catch (e) {
- log(`No history found...`);
- }
- }
-}
-
-module.exports = (enabled, historyFilePath) => {
- log(`Status – ${enabled ? 'enabled' : 'disabled'}`);
-
- if (enabled) {
- return new IncrementalWebpackCompiler(historyFilePath);
- }
- return new NoopCompiler();
-};
diff --git a/config/helpers/incremental_webpack_compiler/compiler.js b/config/helpers/incremental_webpack_compiler/compiler.js
new file mode 100644
index 00000000000..480d7fa3263
--- /dev/null
+++ b/config/helpers/incremental_webpack_compiler/compiler.js
@@ -0,0 +1,117 @@
+/* eslint-disable max-classes-per-file */
+
+const path = require('path');
+const { History, HistoryWithTTL } = require('./history');
+const log = require('./log');
+
+const onRequestEntryPoint = (app, callback) => {
+ app.use((req, res, next) => {
+ const fileName = path.basename(req.url);
+
+ /**
+ * We are only interested in files that have a name like `pages.foo.bar.chunk.js`
+ * because those are the ones corresponding to our entry points.
+ *
+ * This filters out hot update files that are for example named "pages.foo.bar.[hash].hot-update.js"
+ */
+ if (fileName.startsWith('pages.') && fileName.endsWith('.chunk.js')) {
+ const entryPoint = fileName.replace(/\.chunk\.js$/, '');
+ callback(entryPoint);
+ }
+
+ next();
+ });
+};
+
+/**
+ * The NoopCompiler does nothing, following the null object pattern.
+ */
+class NoopCompiler {
+ constructor() {
+ this.enabled = false;
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ filterEntryPoints(entryPoints) {
+ return entryPoints;
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ logStatus() {}
+
+ // eslint-disable-next-line class-methods-use-this
+ setupMiddleware() {}
+}
+
+/**
+ * The HistoryOnlyCompiler only records which entry points have been requested.
+ * This is so that if the user disables incremental compilation, history is
+ * still recorded. If they later enable incremental compilation, that history
+ * can be used.
+ */
+class HistoryOnlyCompiler extends NoopCompiler {
+ constructor(historyFilePath) {
+ super();
+ this.history = new History(historyFilePath);
+ }
+
+ setupMiddleware(app) {
+ onRequestEntryPoint(app, (entryPoint) => {
+ this.history.onRequestEntryPoint(entryPoint);
+ });
+ }
+}
+
+// If we force a recompile immediately, the page reload doesn't seem to work.
+// Five seconds seem to work fine and the user can read the message
+const TIMEOUT = 5000;
+
+/**
+ * The IncrementalWebpackCompiler tracks which entry points have been
+ * requested, and only compiles entry points visited within the last `ttl`
+ * days.
+ */
+class IncrementalWebpackCompiler {
+ constructor(historyFilePath, ttl) {
+ this.enabled = true;
+ this.history = new HistoryWithTTL(historyFilePath, ttl);
+ }
+
+ filterEntryPoints(entrypoints) {
+ return Object.fromEntries(
+ Object.entries(entrypoints).map(([entryPoint, paths]) => {
+ if (this.history.isRecentlyVisited(entryPoint)) {
+ return [entryPoint, paths];
+ }
+ return [entryPoint, ['./webpack_non_compiled_placeholder.js']];
+ }),
+ );
+ }
+
+ logStatus(totalCount) {
+ log(`Currently compiling route entrypoints: ${this.history.size} of ${totalCount}`);
+ }
+
+ setupMiddleware(app, server) {
+ onRequestEntryPoint(app, (entryPoint) => {
+ const wasVisitedRecently = this.history.onRequestEntryPoint(entryPoint);
+ if (!wasVisitedRecently) {
+ log(`Have not visited ${entryPoint} recently. Adding to compilation.`);
+
+ setTimeout(() => {
+ server.middleware.invalidate(() => {
+ if (server.sockets) {
+ server.sockWrite(server.sockets, 'content-changed');
+ }
+ });
+ }, TIMEOUT);
+ }
+ });
+ }
+}
+
+module.exports = {
+ NoopCompiler,
+ HistoryOnlyCompiler,
+ IncrementalWebpackCompiler,
+};
diff --git a/config/helpers/incremental_webpack_compiler/history.js b/config/helpers/incremental_webpack_compiler/history.js
new file mode 100644
index 00000000000..24599900011
--- /dev/null
+++ b/config/helpers/incremental_webpack_compiler/history.js
@@ -0,0 +1,176 @@
+/* eslint-disable max-classes-per-file, no-underscore-dangle */
+
+const fs = require('fs');
+const log = require('./log');
+
+const ESSENTIAL_ENTRY_POINTS = [
+ // Login page
+ 'pages.sessions.new',
+ // Explore page
+ 'pages.root',
+];
+
+// TODO: Find a way to keep this list up-to-date/relevant.
+const COMMON_ENTRY_POINTS = [
+ ...ESSENTIAL_ENTRY_POINTS,
+ 'pages.admin',
+ 'pages.admin.dashboard',
+ 'pages.dashboard.groups.index',
+ 'pages.dashboard.projects.index',
+ 'pages.groups.new',
+ 'pages.groups.show',
+ 'pages.profiles.preferences.show',
+ 'pages.projects.commit.show',
+ 'pages.projects.edit',
+ 'pages.projects.issues.index',
+ 'pages.projects.issues.new',
+ 'pages.projects.issues.show',
+ 'pages.projects.jobs.show',
+ 'pages.projects.merge_requests.index',
+ 'pages.projects.merge_requests.show',
+ 'pages.projects.milestones.index',
+ 'pages.projects.new',
+ 'pages.projects.pipelines.index',
+ 'pages.projects.pipelines.show',
+ 'pages.projects.settings.ci_cd.show',
+ 'pages.projects.settings.repository.show',
+ 'pages.projects.show',
+ 'pages.users',
+];
+
+/**
+ * The History class is responsible for tracking which entry points have been
+ * requested, and persisting/loading the history to/from disk.
+ */
+class History {
+ constructor(historyFilePath) {
+ this._historyFilePath = historyFilePath;
+ this._history = {};
+
+ this._loadHistoryFile();
+ }
+
+ onRequestEntryPoint(entryPoint) {
+ const wasVisitedRecently = this.isRecentlyVisited(entryPoint);
+
+ this._addEntryPoint(entryPoint);
+
+ this._writeHistoryFile();
+
+ return wasVisitedRecently;
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ isRecentlyVisited() {
+ return true;
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ get size() {
+ return 0;
+ }
+
+ // Private methods
+
+ _addEntryPoint(entryPoint) {
+ if (!this._history[entryPoint]) {
+ this._history[entryPoint] = { lastVisit: null, count: 0 };
+ }
+
+ this._history[entryPoint].lastVisit = Date.now();
+ this._history[entryPoint].count += 1;
+ }
+
+ _writeHistoryFile() {
+ try {
+ fs.writeFileSync(this._historyFilePath, JSON.stringify(this._history), 'utf8');
+ } catch (error) {
+ log('Warning – Could not write to history', error.message);
+ }
+ }
+
+ _loadHistoryFile() {
+ try {
+ fs.accessSync(this._historyFilePath);
+ } catch (e) {
+ // History file doesn't exist; attempt to seed it, and return early
+ this._seedHistory();
+ return;
+ }
+
+ // History file already exists; attempt to load its contents into memory
+ try {
+ this._history = JSON.parse(fs.readFileSync(this._historyFilePath, 'utf8'));
+ const historySize = Object.keys(this._history).length;
+ log(`Successfully loaded history containing ${historySize} entry points`);
+ } catch (error) {
+ log('Could not load history', error.message);
+ }
+ }
+
+ /**
+ * Seeds a reasonable set of approximately the most common entry points to
+ * seed the history. This helps to avoid fresh GDK installs showing the
+ * compiling overlay too often.
+ */
+ _seedHistory() {
+ log('Seeding history...');
+ COMMON_ENTRY_POINTS.forEach((entryPoint) => this._addEntryPoint(entryPoint));
+ this._writeHistoryFile();
+ }
+}
+
+const MS_PER_DAY = 1000 * 60 * 60 * 24;
+
+/**
+ * The HistoryWithTTL class adds LRU-like behaviour onto the base History
+ * behaviour. Entry points visited within the last `ttl` days are considered
+ * "recent", and therefore should be eagerly compiled.
+ */
+class HistoryWithTTL extends History {
+ constructor(historyFilePath, ttl) {
+ super(historyFilePath);
+ this._ttl = ttl;
+ this._calculateRecentEntryPoints();
+ }
+
+ onRequestEntryPoint(entryPoint) {
+ const wasVisitedRecently = super.onRequestEntryPoint(entryPoint);
+
+ this._calculateRecentEntryPoints();
+
+ return wasVisitedRecently;
+ }
+
+ isRecentlyVisited(entryPoint) {
+ return this._recentEntryPoints.has(entryPoint);
+ }
+
+ get size() {
+ return this._recentEntryPoints.size;
+ }
+
+ // Private methods
+
+ _calculateRecentEntryPoints() {
+ const oldestVisitAllowed = Date.now() - MS_PER_DAY * this._ttl;
+
+ const recentEntryPoints = Object.entries(this._history).reduce(
+ (acc, [entryPoint, { lastVisit }]) => {
+ if (lastVisit > oldestVisitAllowed) {
+ acc.push(entryPoint);
+ }
+
+ return acc;
+ },
+ [],
+ );
+
+ this._recentEntryPoints = new Set([...ESSENTIAL_ENTRY_POINTS, ...recentEntryPoints]);
+ }
+}
+
+module.exports = {
+ History,
+ HistoryWithTTL,
+};
diff --git a/config/helpers/incremental_webpack_compiler/index.js b/config/helpers/incremental_webpack_compiler/index.js
new file mode 100644
index 00000000000..81826607490
--- /dev/null
+++ b/config/helpers/incremental_webpack_compiler/index.js
@@ -0,0 +1,17 @@
+const { NoopCompiler, HistoryOnlyCompiler, IncrementalWebpackCompiler } = require('./compiler');
+const log = require('./log');
+
+module.exports = (recordHistory, enabled, historyFilePath, ttl) => {
+ if (!recordHistory) {
+ log(`Status – disabled`);
+ return new NoopCompiler();
+ }
+
+ if (enabled) {
+ log(`Status – enabled, ttl=${ttl}`);
+ return new IncrementalWebpackCompiler(historyFilePath, ttl);
+ }
+
+ log(`Status – history-only`);
+ return new HistoryOnlyCompiler(historyFilePath);
+};
diff --git a/config/helpers/incremental_webpack_compiler/log.js b/config/helpers/incremental_webpack_compiler/log.js
new file mode 100644
index 00000000000..6336cb0a78b
--- /dev/null
+++ b/config/helpers/incremental_webpack_compiler/log.js
@@ -0,0 +1,3 @@
+const log = (msg, ...rest) => console.log(`IncrementalWebpackCompiler: ${msg}`, ...rest);
+
+module.exports = log;
diff --git a/config/routes/user.rb b/config/routes/user.rb
index 109179f76f1..7d61aa089fb 100644
--- a/config/routes/user.rb
+++ b/config/routes/user.rb
@@ -64,7 +64,7 @@ constraints(::Constraints::UserUrlConstrainer.new) do
get ':username.keys' => 'users#ssh_keys', constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }
# Get all GPG keys of user
- get ':username.gpg' => 'users#gpg_keys', constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }
+ get ':username.gpg' => 'users#gpg_keys', as: 'user_gpg_keys', constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }
scope(path: ':username',
as: :user,
diff --git a/config/webpack.config.js b/config/webpack.config.js
index b81b5611041..f5cd0b154e4 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -48,6 +48,8 @@ const INCREMENTAL_COMPILER_ENABLED =
IS_DEV_SERVER &&
process.env.DEV_SERVER_INCREMENTAL &&
process.env.DEV_SERVER_INCREMENTAL !== 'false';
+const INCREMENTAL_COMPILER_TTL = Number(process.env.DEV_SERVER_INCREMENTAL_TTL) || Infinity;
+const INCREMENTAL_COMPILER_RECORD_HISTORY = IS_DEV_SERVER && !process.env.CI;
const WEBPACK_REPORT = process.env.WEBPACK_REPORT && process.env.WEBPACK_REPORT !== 'false';
const WEBPACK_MEMORY_TEST =
process.env.WEBPACK_MEMORY_TEST && process.env.WEBPACK_MEMORY_TEST !== 'false';
@@ -69,8 +71,10 @@ let watchAutoEntries = [];
const defaultEntries = ['./main'];
const incrementalCompiler = createIncrementalWebpackCompiler(
+ INCREMENTAL_COMPILER_RECORD_HISTORY,
INCREMENTAL_COMPILER_ENABLED,
path.join(CACHE_PATH, 'incremental-webpack-compiler-history.json'),
+ INCREMENTAL_COMPILER_TTL,
);
function generateEntries() {
@@ -157,6 +161,9 @@ const alias = {
// the following resolves files which are different between CE and EE
ee_else_ce: path.join(ROOT_PATH, 'app/assets/javascripts'),
+ // the following resolves files which are different between CE and JH
+ jh_else_ce: path.join(ROOT_PATH, 'app/assets/javascripts'),
+
// override loader path for icons.svg so we do not duplicate this asset
'@gitlab/svgs/dist/icons.svg': path.join(
ROOT_PATH,
@@ -180,10 +187,13 @@ if (IS_EE) {
if (IS_JH) {
Object.assign(alias, {
jh: path.join(ROOT_PATH, 'jh/app/assets/javascripts'),
+ jh_component: path.join(ROOT_PATH, 'jh/app/assets/javascripts'),
+ jh_empty_states: path.join(ROOT_PATH, 'jh/app/views/shared/empty_states'),
jh_icons: path.join(ROOT_PATH, 'jh/app/views/shared/icons'),
jh_images: path.join(ROOT_PATH, 'jh/app/assets/images'),
jh_spec: path.join(ROOT_PATH, 'jh/spec/javascripts'),
jh_jest: path.join(ROOT_PATH, 'jh/spec/frontend'),
+ jh_else_ce: path.join(ROOT_PATH, 'jh/app/assets/javascripts'),
});
}
@@ -519,6 +529,15 @@ module.exports = {
);
}),
+ !IS_JH &&
+ new webpack.NormalModuleReplacementPlugin(/^jh_component\/(.*)\.vue/, (resource) => {
+ // eslint-disable-next-line no-param-reassign
+ resource.request = path.join(
+ ROOT_PATH,
+ 'app/assets/javascripts/vue_shared/components/empty_component.js',
+ );
+ }),
+
new CopyWebpackPlugin({
patterns: [
{
@@ -634,10 +653,12 @@ module.exports = {
}),
new webpack.DefinePlugin({
- // This one is used to define window.gon.ee and other things properly in tests:
+ // These are used to define window.gon.ee, window.gon.jh and other things properly in tests:
'process.env.IS_EE': JSON.stringify(IS_EE),
- // This one is used to check against "EE" properly in application code
+ 'process.env.IS_JH': JSON.stringify(IS_JH),
+ // These are used to check against "EE" properly in application code
IS_EE: IS_EE ? 'window.gon && window.gon.ee' : JSON.stringify(false),
+ IS_JH: IS_JH ? 'window.gon && window.gon.jh' : JSON.stringify(false),
// This is used by Sourcegraph because these assets are loaded dnamically
'process.env.SOURCEGRAPH_PUBLIC_PATH': JSON.stringify(SOURCEGRAPH_PUBLIC_PATH),
}),
diff --git a/doc/user/project/repository/gpg_signed_commits/index.md b/doc/user/project/repository/gpg_signed_commits/index.md
index c41b3ed8615..91d3a06d7da 100644
--- a/doc/user/project/repository/gpg_signed_commits/index.md
+++ b/doc/user/project/repository/gpg_signed_commits/index.md
@@ -19,6 +19,11 @@ NOTE:
The term GPG is used for all OpenPGP/PGP/GPG related material and
implementations.
+To view a user's public GPG key, you can:
+
+- Go to `https://gitlab.example.com/<username>.gpg`.
+- Select **View public GPG keys** (**{key}**) in the top right of the user's profile.
+
GPG verified tags are not supported yet.
See the [further reading](#further-reading) section for more details on GPG.
diff --git a/jest.config.base.js b/jest.config.base.js
index 997f3c254b4..3ace87c49bc 100644
--- a/jest.config.base.js
+++ b/jest.config.base.js
@@ -1,10 +1,12 @@
const IS_EE = require('./config/helpers/is_ee_env');
const isESLint = require('./config/helpers/is_eslint');
+const IS_JH = require('./config/helpers/is_jh_env');
module.exports = (path, options = {}) => {
const {
moduleNameMapper: extModuleNameMapper = {},
moduleNameMapperEE: extModuleNameMapperEE = {},
+ moduleNameMapperJH: extModuleNameMapperJH = {},
} = options;
const reporters = ['default'];
@@ -29,6 +31,9 @@ module.exports = (path, options = {}) => {
testMatch.push(`<rootDir>/ee/${glob}`);
}
+ if (IS_JH) {
+ testMatch.push(`<rootDir>/jh/${glob}`);
+ }
// workaround for eslint-import-resolver-jest only resolving in test files
// see https://github.com/JoinColony/eslint-import-resolver-jest#note
if (isESLint(module)) {
@@ -41,8 +46,11 @@ module.exports = (path, options = {}) => {
'^~(/.*)$': '<rootDir>/app/assets/javascripts$1',
'^ee_component(/.*)$':
'<rootDir>/app/assets/javascripts/vue_shared/components/empty_component.js',
+ '^jh_component(/.*)$':
+ '<rootDir>/app/assets/javascripts/vue_shared/components/empty_component.js',
'^shared_queries(/.*)$': '<rootDir>/app/graphql/queries$1',
'^ee_else_ce(/.*)$': '<rootDir>/app/assets/javascripts$1',
+ '^jh_else_ce(/.*)$': '<rootDir>/app/assets/javascripts$1',
'^helpers(/.*)$': '<rootDir>/spec/frontend/__helpers__$1',
'^vendor(/.*)$': '<rootDir>/vendor/assets/javascripts$1',
[TEST_FIXTURES_PATTERN]: '<rootDir>/tmp/tests/frontend/fixtures$1',
@@ -70,6 +78,19 @@ module.exports = (path, options = {}) => {
collectCoverageFrom.push(rootDirEE.replace('$1', '/**/*.{js,vue}'));
}
+ if (IS_JH) {
+ const rootDirJH = '<rootDir>/jh/app/assets/javascripts$1';
+ Object.assign(moduleNameMapper, {
+ '^jh(/.*)$': rootDirJH,
+ '^jh_component(/.*)$': rootDirJH,
+ '^jh_else_ce(/.*)$': rootDirJH,
+ '^jh_jest/(.*)$': '<rootDir>/jh/spec/frontend/$1',
+ ...extModuleNameMapperJH,
+ });
+
+ collectCoverageFrom.push(rootDirJH.replace('$1', '/**/*.{js,vue}'));
+ }
+
const coverageDirectory = () => {
if (process.env.CI_NODE_INDEX && process.env.CI_NODE_TOTAL) {
return `<rootDir>/coverage-frontend/jest-${process.env.CI_NODE_INDEX}-${process.env.CI_NODE_TOTAL}`;
@@ -107,6 +128,7 @@ module.exports = (path, options = {}) => {
testEnvironment: '<rootDir>/spec/frontend/environment.js',
testEnvironmentOptions: {
IS_EE,
+ IS_JH,
},
};
};
diff --git a/jest.config.integration.js b/jest.config.integration.js
index 92296fb751e..da8e813a2cb 100644
--- a/jest.config.integration.js
+++ b/jest.config.integration.js
@@ -8,9 +8,13 @@ module.exports = {
moduleNameMapper: {
'^test_helpers(/.*)$': '<rootDir>/spec/frontend_integration/test_helpers$1',
'^ee_else_ce_test_helpers(/.*)$': '<rootDir>/spec/frontend_integration/test_helpers$1',
+ '^jh_else_ce_test_helpers(/.*)$': '<rootDir>/spec/frontend_integration/test_helpers$1',
},
moduleNameMapperEE: {
'^ee_else_ce_test_helpers(/.*)$': '<rootDir>/ee/spec/frontend_integration/test_helpers$1',
},
+ moduleNameMapperJH: {
+ '^jh_else_ce_test_helpers(/.*)$': '<rootDir>/jh/spec/frontend_integration/test_helpers$1',
+ },
}),
};
diff --git a/lib/api/helm_packages.rb b/lib/api/helm_packages.rb
index 4280744d8b4..d6281c3772f 100644
--- a/lib/api/helm_packages.rb
+++ b/lib/api/helm_packages.rb
@@ -66,7 +66,7 @@ module API
get ":channel/charts/:file_name.tgz", requirements: FILE_NAME_REQUIREMENTS do
authorize_read_package!(authorized_user_project)
- package_file = Packages::Helm::PackageFilesFinder.new(authorized_user_project, params[:channel], file_name: "#{params[:file_name]}.tgz").execute.last!
+ package_file = Packages::Helm::PackageFilesFinder.new(authorized_user_project, params[:channel], file_name: "#{params[:file_name]}.tgz").most_recent!
track_package_event('pull_package', :helm, project: authorized_user_project, namespace: authorized_user_project.namespace)
diff --git a/lib/gitlab/database/load_balancing/service_discovery.rb b/lib/gitlab/database/load_balancing/service_discovery.rb
index f04ecfa5e72..dfd4892371c 100644
--- a/lib/gitlab/database/load_balancing/service_discovery.rb
+++ b/lib/gitlab/database/load_balancing/service_discovery.rb
@@ -89,8 +89,10 @@ module Gitlab
# Sentry, instead of silently terminating this thread.
Gitlab::ErrorTracking.track_exception(error)
- Gitlab::AppLogger.error(
- "Service discovery encountered an error: #{error.message}"
+ Gitlab::Database::LoadBalancing::Logger.error(
+ event: :service_discovery_failure,
+ message: "Service discovery encountered an error: #{error.message}",
+ host_list_length: load_balancer.host_list.length
)
# Slightly randomize the retry delay so that, in the case of a total
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 452f8d8f72e..861ca1e757c 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -37,6 +37,7 @@ module Gitlab
gon.first_day_of_week = current_user&.first_day_of_week || Gitlab::CurrentSettings.first_day_of_week
gon.time_display_relative = true
gon.ee = Gitlab.ee?
+ gon.jh = Gitlab.jh?
gon.dot_com = Gitlab.com?
if current_user
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index db78be8c0af..c3d05698790 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -36989,6 +36989,11 @@ msgstr ""
msgid "View project labels"
msgstr ""
+msgid "View public GPG key"
+msgid_plural "View public GPG keys"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "View replaced file @ "
msgstr ""
diff --git a/spec/features/users/show_spec.rb b/spec/features/users/show_spec.rb
index deb25b675a0..6f66cf5e3ae 100644
--- a/spec/features/users/show_spec.rb
+++ b/spec/features/users/show_spec.rb
@@ -429,4 +429,27 @@ RSpec.describe 'User page' do
end
end
end
+
+ context 'GPG keys' do
+ context 'when user has verified GPG keys' do
+ let_it_be(:user) { create(:user, email: GpgHelpers::User1.emails.first) }
+ let_it_be(:gpg_key) { create(:gpg_key, user: user, key: GpgHelpers::User1.public_key) }
+ let_it_be(:gpg_key2) { create(:gpg_key, user: user, key: GpgHelpers::User1.public_key2) }
+
+ it 'shows link to public GPG keys' do
+ subject
+
+ expect(page).to have_link('View public GPG keys', href: user_gpg_keys_path(user))
+ end
+ end
+
+ context 'when user does not have verified GPG keys' do
+ it 'does not show link to public GPG keys' do
+ subject
+
+ expect(page).not_to have_link('View public GPG key', href: user_gpg_keys_path(user))
+ expect(page).not_to have_link('View public GPG keys', href: user_gpg_keys_path(user))
+ end
+ end
+ end
end
diff --git a/spec/finders/packages/helm/package_files_finder_spec.rb b/spec/finders/packages/helm/package_files_finder_spec.rb
index 2b84fd2b2d2..5f1378f837d 100644
--- a/spec/finders/packages/helm/package_files_finder_spec.rb
+++ b/spec/finders/packages/helm/package_files_finder_spec.rb
@@ -6,42 +6,51 @@ RSpec.describe ::Packages::Helm::PackageFilesFinder do
let_it_be(:project1) { create(:project) }
let_it_be(:project2) { create(:project) }
let_it_be(:helm_package) { create(:helm_package, project: project1) }
- let_it_be(:helm_package_file) { helm_package.package_files.first }
+ let_it_be(:helm_package_file1) { helm_package.package_files.first }
+ let_it_be(:helm_package_file2) { create(:helm_package_file, package: helm_package) }
let_it_be(:debian_package) { create(:debian_package, project: project1) }
- describe '#execute' do
- let(:project) { project1 }
- let(:channel) { 'stable' }
- let(:params) { {} }
+ let(:project) { project1 }
+ let(:channel) { 'stable' }
+ let(:params) { {} }
+
+ let(:service) { described_class.new(project, channel, params) }
- subject { described_class.new(project, channel, params).execute }
+ describe '#execute' do
+ subject { service.execute }
context 'with empty params' do
- it { is_expected.to match_array([helm_package_file]) }
+ it { is_expected.to eq([helm_package_file2, helm_package_file1]) }
end
context 'with another project' do
let(:project) { project2 }
- it { is_expected.to match_array([]) }
+ it { is_expected.to eq([]) }
end
context 'with another channel' do
let(:channel) { 'staging' }
- it { is_expected.to match_array([]) }
+ it { is_expected.to eq([]) }
end
- context 'with file_name' do
- let(:params) { { file_name: helm_package_file.file_name } }
+ context 'with matching file_name' do
+ let(:params) { { file_name: helm_package_file1.file_name } }
- it { is_expected.to match_array([helm_package_file]) }
+ it { is_expected.to eq([helm_package_file2, helm_package_file1]) }
end
context 'with another file_name' do
let(:params) { { file_name: 'foobar.tgz' } }
- it { is_expected.to match_array([]) }
+ it { is_expected.to eq([]) }
end
end
+
+ describe '#most_recent!' do
+ subject { service.most_recent! }
+
+ it { is_expected.to eq(helm_package_file2) }
+ end
end
diff --git a/spec/frontend/tracking_spec.js b/spec/frontend/tracking_spec.js
index a17efdd61a9..f3233bcd771 100644
--- a/spec/frontend/tracking_spec.js
+++ b/spec/frontend/tracking_spec.js
@@ -349,7 +349,7 @@ describe('Tracking', () => {
it('includes experiment data if linked to an experiment', () => {
const mockExperimentData = {
variant: 'candidate',
- experiment: 'repo_integrations_link',
+ experiment: 'example',
key: '2bff73f6bb8cc11156c50a8ba66b9b8b',
};
getExperimentData.mockReturnValue(mockExperimentData);
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
index 90bc1980ac3..843298a1406 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
@@ -7,7 +7,12 @@ import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue';
import createLabelMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql';
-import { mockSuggestedColors, createLabelSuccessfulResponse } from './mock_data';
+import projectLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
+import {
+ mockSuggestedColors,
+ createLabelSuccessfulResponse,
+ labelsQueryResponse,
+} from './mock_data';
jest.mock('~/flash');
@@ -44,6 +49,14 @@ describe('DropdownContentsCreateView', () => {
const createComponent = ({ mutationHandler = createLabelSuccessHandler } = {}) => {
const mockApollo = createMockApollo([[createLabelMutation, mutationHandler]]);
+ mockApollo.clients.defaultClient.cache.writeQuery({
+ query: projectLabelsQuery,
+ data: labelsQueryResponse.data,
+ variables: {
+ fullPath: '',
+ searchTerm: '',
+ },
+ });
wrapper = shallowMount(DropdownContentsCreateView, {
localVue,
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
index 8bd944a3d54..6279fe8ed57 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
@@ -137,12 +137,6 @@ describe('DropdownContentsLabelsView', () => {
expect(findLabels().at(0).attributes('islabelset')).toBe('true');
});
-
- it('emits `closeDropdown event` when Esc button is pressed', () => {
- findDropdownWrapper().trigger('keydown.esc');
-
- expect(wrapper.emitted('closeDropdown')).toEqual([[selectedLabels]]);
- });
});
it('when search returns 0 results', async () => {
@@ -205,7 +199,7 @@ describe('DropdownContentsLabelsView', () => {
});
it('emits `toggleDropdownContentsCreateView` event on create label button click', () => {
- findCreateLabelButton().vm.$emit('click');
+ findCreateLabelButton().vm.$emit('click', new MouseEvent('click'));
expect(wrapper.emitted('toggleDropdownContentsCreateView')).toEqual([[]]);
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_title_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_title_spec.js
deleted file mode 100644
index d2401a1f725..00000000000
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_title_spec.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import { GlButton, GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
-
-import DropdownTitle from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue';
-
-import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_widget/store';
-
-import { mockConfig } from './mock_data';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-const createComponent = (initialState = mockConfig) => {
- const store = new Vuex.Store(labelsSelectModule());
-
- store.dispatch('setInitialState', initialState);
-
- return shallowMount(DropdownTitle, {
- localVue,
- store,
- propsData: {
- labelsSelectInProgress: false,
- },
- });
-};
-
-describe('DropdownTitle', () => {
- let wrapper;
-
- beforeEach(() => {
- wrapper = createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('template', () => {
- it('renders component container element with string "Labels"', () => {
- expect(wrapper.text()).toContain('Labels');
- });
-
- it('renders edit link', () => {
- const editBtnEl = wrapper.find(GlButton);
-
- expect(editBtnEl.exists()).toBe(true);
- expect(editBtnEl.text()).toBe('Edit');
- });
-
- it('renders loading icon element when `labelsSelectInProgress` prop is true', () => {
- wrapper.setProps({
- labelsSelectInProgress: true,
- });
-
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true);
- });
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
index e17dfd93efc..d91d87ee6f0 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
@@ -1,11 +1,7 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-
-import { isInViewport } from '~/lib/utils/common_utils';
-import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
-import DropdownButton from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_button.vue';
+import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue';
-import DropdownTitle from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue';
import DropdownValue from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue';
import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value_collapsed.vue';
import LabelsSelectRoot from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
@@ -32,11 +28,13 @@ describe('LabelsSelectRoot', () => {
store,
propsData: config,
stubs: {
- 'dropdown-contents': DropdownContents,
+ DropdownContents,
+ SidebarEditableItem,
},
provide: {
iid: '1',
projectPath: 'test',
+ canUpdate: true,
},
});
};
@@ -49,145 +47,44 @@ describe('LabelsSelectRoot', () => {
wrapper.destroy();
});
- describe('methods', () => {
- describe('handleDropdownClose', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('emits `updateSelectedLabels` & `onDropdownClose` events on component when provided `labels` param is not empty', () => {
- wrapper.vm.handleDropdownClose([{ id: 1 }, { id: 2 }]);
-
- expect(wrapper.emitted().updateSelectedLabels).toBeTruthy();
- expect(wrapper.emitted().onDropdownClose).toBeTruthy();
- });
-
- it('emits only `onDropdownClose` event on component when provided `labels` param is empty', () => {
- wrapper.vm.handleDropdownClose([]);
-
- expect(wrapper.emitted().updateSelectedLabels).toBeFalsy();
- expect(wrapper.emitted().onDropdownClose).toBeTruthy();
- });
- });
-
- describe('handleCollapsedValueClick', () => {
- it('emits `toggleCollapse` event on component', () => {
- createComponent();
- wrapper.vm.handleCollapsedValueClick();
-
- expect(wrapper.emitted().toggleCollapse).toBeTruthy();
- });
- });
+ it('renders component with classes `labels-select-wrapper position-relative`', () => {
+ createComponent();
+ expect(wrapper.classes()).toEqual(['labels-select-wrapper', 'position-relative']);
});
- describe('template', () => {
- it('renders component with classes `labels-select-wrapper position-relative`', () => {
- createComponent();
- expect(wrapper.attributes('class')).toContain('labels-select-wrapper position-relative');
- });
-
- it.each`
- variant | cssClass
- ${'standalone'} | ${'is-standalone'}
- ${'embedded'} | ${'is-embedded'}
- `(
- 'renders component root element with CSS class `$cssClass` when `state.variant` is "$variant"',
- ({ variant, cssClass }) => {
- createComponent({
- ...mockConfig,
- variant,
- });
-
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.classes()).toContain(cssClass);
- });
- },
- );
-
- it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', async () => {
- createComponent();
- await wrapper.vm.$nextTick;
- expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true);
- });
-
- it('renders `dropdown-title` component', async () => {
- createComponent();
- await wrapper.vm.$nextTick;
- expect(wrapper.find(DropdownTitle).exists()).toBe(true);
- });
-
- it('renders `dropdown-value` component', async () => {
- createComponent(mockConfig, {
- default: 'None',
+ it.each`
+ variant | cssClass
+ ${'standalone'} | ${'is-standalone'}
+ ${'embedded'} | ${'is-embedded'}
+ `(
+ 'renders component root element with CSS class `$cssClass` when `state.variant` is "$variant"',
+ ({ variant, cssClass }) => {
+ createComponent({
+ ...mockConfig,
+ variant,
});
- await wrapper.vm.$nextTick;
-
- const valueComp = wrapper.find(DropdownValue);
-
- expect(valueComp.exists()).toBe(true);
- expect(valueComp.text()).toBe('None');
- });
-
- it('renders `dropdown-button` component when `showDropdownButton` prop is `true`', async () => {
- createComponent();
- wrapper.vm.$store.dispatch('toggleDropdownButton');
- await wrapper.vm.$nextTick;
- expect(wrapper.find(DropdownButton).exists()).toBe(true);
- });
-
- it('renders `dropdown-contents` component when `showDropdownButton` & `showDropdownContents` prop is `true`', async () => {
- createComponent();
- wrapper.vm.$store.dispatch('toggleDropdownContents');
- await wrapper.vm.$nextTick;
- expect(wrapper.find(DropdownContents).exists()).toBe(true);
- });
-
- describe('sets content direction based on viewport', () => {
- describe.each(Object.values(DropdownVariant))(
- 'when labels variant is "%s"',
- ({ variant }) => {
- beforeEach(() => {
- createComponent({ ...mockConfig, variant });
- wrapper.vm.$store.dispatch('toggleDropdownContents');
- });
- it('set direction when out of viewport', () => {
- isInViewport.mockImplementation(() => false);
- wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state);
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(true);
- });
- });
-
- it('does not set direction when inside of viewport', () => {
- isInViewport.mockImplementation(() => true);
- wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state);
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(false);
- });
- });
- },
- );
- });
- });
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.classes()).toContain(cssClass);
+ });
+ },
+ );
- it('calls toggleDropdownContents action when isEditing prop is changing to true', async () => {
+ it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', async () => {
createComponent();
-
- jest.spyOn(store, 'dispatch').mockResolvedValue();
- await wrapper.setProps({ isEditing: true });
-
- expect(store.dispatch).toHaveBeenCalledWith('toggleDropdownContents');
+ await wrapper.vm.$nextTick;
+ expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true);
});
- it('does not call toggleDropdownContents action when isEditing prop is changing to false', async () => {
- createComponent();
+ it('renders `dropdown-value` component', async () => {
+ createComponent(mockConfig, {
+ default: 'None',
+ });
+ await wrapper.vm.$nextTick;
- jest.spyOn(store, 'dispatch').mockResolvedValue();
- await wrapper.setProps({ isEditing: false });
+ const valueComp = wrapper.find(DropdownValue);
- expect(store.dispatch).not.toHaveBeenCalled();
+ expect(valueComp.exists()).toBe(true);
+ expect(valueComp.text()).toBe('None');
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
index 5dd8fc1b8b2..982b2e15e46 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
@@ -83,9 +83,7 @@ export const createLabelSuccessfulResponse = {
id: 'gid://gitlab/ProjectLabel/126',
color: '#dc143c',
description: null,
- descriptionHtml: '',
title: 'ewrwrwer',
- textColor: '#FFFFFF',
__typename: 'Label',
},
errors: [],
diff --git a/spec/models/packages/package_file_spec.rb b/spec/models/packages/package_file_spec.rb
index 90910fcb7ce..f9fd59551d1 100644
--- a/spec/models/packages/package_file_spec.rb
+++ b/spec/models/packages/package_file_spec.rb
@@ -139,6 +139,10 @@ RSpec.describe Packages::PackageFile, type: :model do
end
end
+ describe '.most_recent!' do
+ it { expect(described_class.most_recent!).to eq(debian_package.package_files.last) }
+ end
+
describe '#update_file_store callback' do
let_it_be(:package_file) { build(:package_file, :nuget, size: nil) }
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 5245df10a71..9a6fd67d6c5 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -3389,17 +3389,32 @@ RSpec.describe User do
end
describe '#membership_groups' do
- let!(:user) { create(:user) }
- let!(:parent_group) { create(:group) }
- let!(:child_group) { create(:group, parent: parent_group) }
+ let_it_be(:user) { create(:user) }
- before do
- parent_group.add_user(user, Gitlab::Access::MAINTAINER)
+ let_it_be(:parent_group) do
+ create(:group).tap do |g|
+ g.add_user(user, Gitlab::Access::MAINTAINER)
+ end
end
+ let_it_be(:child_group) { create(:group, parent: parent_group) }
+ let_it_be(:other_group) { create(:group) }
+
subject { user.membership_groups }
- it { is_expected.to contain_exactly parent_group, child_group }
+ shared_examples 'returns groups where the user is a member' do
+ specify { is_expected.to contain_exactly(parent_group, child_group) }
+ end
+
+ it_behaves_like 'returns groups where the user is a member'
+
+ context 'when feature flag :linear_user_membership_groups is disabled' do
+ before do
+ stub_feature_flags(linear_user_membership_groups: false)
+ end
+
+ it_behaves_like 'returns groups where the user is a member'
+ end
end
describe '#authorizations_for_projects' do
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index fd75c8411d5..5f789f59908 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -649,36 +649,18 @@ RSpec.describe ProjectPresenter do
end
end
- describe 'experiment(:repo_integrations_link)' do
- context 'when enabled' do
- before do
- stub_experiments(repo_integrations_link: :candidate)
- end
-
- it 'includes a button to configure integrations for maintainers' do
- project.add_maintainer(user)
-
- expect(empty_repo_statistics_buttons.map(&:label)).to include(
- a_string_including('Configure Integration')
- )
- end
-
- it 'does not include a button if not a maintainer' do
- expect(empty_repo_statistics_buttons.map(&:label)).not_to include(
- a_string_including('Configure Integration')
- )
- end
- end
+ it 'includes a button to configure integrations for maintainers' do
+ project.add_maintainer(user)
- context 'when disabled' do
- it 'does not include a button' do
- project.add_maintainer(user)
+ expect(empty_repo_statistics_buttons.map(&:label)).to include(
+ a_string_including('Configure Integration')
+ )
+ end
- expect(empty_repo_statistics_buttons.map(&:label)).not_to include(
- a_string_including('Configure Integration')
- )
- end
- end
+ it 'does not include a button if not a maintainer' do
+ expect(empty_repo_statistics_buttons.map(&:label)).not_to include(
+ a_string_including('Configure Integration')
+ )
end
context 'for a developer' do
diff --git a/spec/requests/api/helm_packages_spec.rb b/spec/requests/api/helm_packages_spec.rb
index 08b4489a6e3..7b37a95b204 100644
--- a/spec/requests/api/helm_packages_spec.rb
+++ b/spec/requests/api/helm_packages_spec.rb
@@ -9,16 +9,18 @@ RSpec.describe API::HelmPackages do
let_it_be_with_reload(:project) { create(:project, :public) }
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
- let_it_be(:package) { create(:helm_package, project: project) }
+ let_it_be(:package) { create(:helm_package, project: project, without_package_files: true) }
+ let_it_be(:package_file1) { create(:helm_package_file, package: package) }
+ let_it_be(:package_file2) { create(:helm_package_file, package: package) }
describe 'GET /api/v4/projects/:id/packages/helm/:channel/index.yaml' do
it_behaves_like 'handling helm chart index requests' do
- let(:url) { "/projects/#{project.id}/packages/helm/#{package.package_files.first.helm_channel}/index.yaml" }
+ let(:url) { "/projects/#{project.id}/packages/helm/stable/index.yaml" }
end
end
describe 'GET /api/v4/projects/:id/packages/helm/:channel/charts/:file_name.tgz' do
- let(:url) { "/projects/#{project.id}/packages/helm/#{package.package_files.first.helm_channel}/charts/#{package.name}-#{package.version}.tgz" }
+ let(:url) { "/projects/#{project.id}/packages/helm/stable/charts/#{package.name}-#{package.version}.tgz" }
subject { get api(url), headers: headers }
diff --git a/spec/support/database/prevent_cross_joins.rb b/spec/support/database/prevent_cross_joins.rb
index 8ec946997fb..d5ec3f61570 100644
--- a/spec/support/database/prevent_cross_joins.rb
+++ b/spec/support/database/prevent_cross_joins.rb
@@ -72,13 +72,10 @@ ALLOW_LIST = Set.new(YAML.load_file(Rails.root.join('.cross-join-allowlist.yml')
RSpec.configure do |config|
config.include(::Database::PreventCrossJoins::SpecHelpers)
- config.around do |example|
+ # TODO: remove `:prevent_cross_joins` to enable the check by default
+ config.around(:each, :prevent_cross_joins) do |example|
Thread.current[:has_cross_join_exception] = false
- if ALLOW_LIST.include?(example.file_path)
- example.run
- else
- with_cross_joins_prevented { example.run }
- end
+ with_cross_joins_prevented { example.run }
end
end
diff --git a/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb
index 1ad38a17f9c..d4801af4980 100644
--- a/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb
@@ -41,7 +41,7 @@ RSpec.shared_examples 'process helm service index request' do |user_type, status
package_entry = yaml_response['entries'][package.name]
- expect(package_entry.length).to eq(1)
+ expect(package_entry.length).to eq(2)
expect(package_entry.first.keys).to contain_exactly('name', 'version', 'apiVersion', 'created', 'digest', 'urls')
expect(package_entry.first['digest']).to eq('fd2b2fa0329e80a2a602c2bb3b40608bcd6ee5cf96cf46fd0d2800a4c129c9db')
expect(package_entry.first['urls']).to eq(["charts/#{package.name}-#{package.version}.tgz"])
@@ -174,6 +174,13 @@ RSpec.shared_examples 'process helm download content request' do |user_type, sta
context "for user type #{user_type}" do
before do
project.send("add_#{user_type}", user) if user_type != :anonymous && user_type != :not_a_member
+
+ expect_next_found_instance_of(::Packages::PackageFile) do |package_file|
+ expect(package_file).to receive(:file).and_wrap_original do |m, *args|
+ expect(package_file.id).to eq(package_file2.id)
+ m.call(*args)
+ end
+ end
end
it_behaves_like 'a package tracking event', 'API::HelmPackages', 'pull_package'
diff --git a/spec/support_specs/database/prevent_cross_joins_spec.rb b/spec/support_specs/database/prevent_cross_joins_spec.rb
index b26b862b34b..dd4ed9c40b8 100644
--- a/spec/support_specs/database/prevent_cross_joins_spec.rb
+++ b/spec/support_specs/database/prevent_cross_joins_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Database::PreventCrossJoins do
- context 'when running in a default scope' do
+ context 'when running in :prevent_cross_joins scope', :prevent_cross_joins do
context 'when only non-CI tables are used' do
it 'does not raise exception' do
expect { main_only_query }.not_to raise_error
@@ -32,6 +32,14 @@ RSpec.describe Database::PreventCrossJoins do
end
end
+ context 'when running in a default scope' do
+ context 'when CI and non-CI tables are used' do
+ it 'does not raise exception' do
+ expect { main_and_ci_query }.not_to raise_error
+ end
+ end
+ end
+
private
def main_only_query
diff --git a/spec/workers/packages/helm/extraction_worker_spec.rb b/spec/workers/packages/helm/extraction_worker_spec.rb
index 258413a3410..daebbda3077 100644
--- a/spec/workers/packages/helm/extraction_worker_spec.rb
+++ b/spec/workers/packages/helm/extraction_worker_spec.rb
@@ -23,10 +23,10 @@ RSpec.describe Packages::Helm::ExtractionWorker, type: :worker do
subject { described_class.new.perform(channel, package_file_id) }
- shared_examples 'handling error' do
+ shared_examples 'handling error' do |error_class = Packages::Helm::ExtractFileMetadataService::ExtractionError|
it 'mark the package as errored', :aggregate_failures do
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
- instance_of(Packages::Helm::ExtractFileMetadataService::ExtractionError),
+ instance_of(error_class),
project_id: package_file.package.project_id
)
expect { subject }
@@ -88,5 +88,15 @@ RSpec.describe Packages::Helm::ExtractionWorker, type: :worker do
it_behaves_like 'handling error'
end
+
+ context 'with an invalid Chart.yaml' do
+ before do
+ expect_next_instance_of(Gem::Package::TarReader::Entry) do |entry|
+ expect(entry).to receive(:read).and_return('{}')
+ end
+ end
+
+ it_behaves_like 'handling error', ActiveRecord::RecordInvalid
+ end
end
end