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>2023-04-12 15:08:27 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-04-12 15:08:27 +0300
commite0d7577e29dcab90623e1f38cf11b351c665ee23 (patch)
tree5a34f26be66301f1af9e36b10a67dfca01fed8ec
parent60e7627c998b74d48df10b9a7759d6038a1f139c (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/frontend.gitlab-ci.yml25
-rw-r--r--.gitlab/ci/review-apps/dast.gitlab-ci.yml10
-rw-r--r--app/assets/javascripts/diffs/components/app.vue112
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue6
-rw-r--r--app/assets/javascripts/diffs/components/tree_list.vue6
-rw-r--r--app/assets/javascripts/diffs/store/actions.js15
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue15
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js2
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql1
-rw-r--r--app/assets/javascripts/super_sidebar/components/groups_list.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/components/projects_list.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue15
-rw-r--r--app/assets/stylesheets/page_bundles/wiki.scss11
-rw-r--r--app/controllers/projects/blame_controller.rb11
-rw-r--r--app/controllers/projects/merge_requests_controller.rb1
-rw-r--r--app/finders/group_members_finder.rb54
-rw-r--r--app/helpers/blame_helper.rb4
-rw-r--r--app/helpers/merge_requests_helper.rb4
-rw-r--r--app/models/packages/event.rb1
-rw-r--r--app/services/projects/blame_service.rb35
-rw-r--r--app/views/projects/blame/show.html.haml12
-rw-r--r--app/views/projects/merge_requests/_page.html.haml2
-rw-r--r--config/events/20230327141223_API__NpmProjectPackages_list_tags.yml25
-rw-r--r--config/events/20230327141524_API__NpmProjectPackages_create_tag.yml25
-rw-r--r--config/events/20230327141627_API__NpmProjectPackages_delete_tag.yml25
-rw-r--r--config/events/20230327142004_API__NpmInstancePackages_list_tags.yml25
-rw-r--r--config/events/20230327142151_API__NpmInstancePackages_create_tag.yml25
-rw-r--r--config/events/20230327142237_API__NpmInstancePackages_delete_tag.yml25
-rw-r--r--config/feature_flags/development/ai_experimentation_api.yml8
-rw-r--r--config/feature_flags/development/members_with_shared_group_access.yml8
-rw-r--r--config/feature_flags/development/single_file_file_by_file.yml8
-rw-r--r--doc/.vale/gitlab/Uppercase.yml1
-rw-r--r--doc/administration/geo/replication/single_sign_on.md9
-rw-r--r--doc/ci/cloud_services/google_cloud/index.md5
-rw-r--r--doc/ci/cloud_services/index.md6
-rw-r--r--doc/ci/examples/authenticating-with-hashicorp-vault/index.md8
-rw-r--r--doc/ci/secrets/index.md11
-rw-r--r--doc/ci/variables/index.md3
-rw-r--r--doc/ci/variables/predefined_variables.md6
-rw-r--r--doc/development/application_secrets.md2
-rw-r--r--doc/development/fe_guide/style/vue.md12
-rw-r--r--doc/integration/jira/configure.md13
-rw-r--r--doc/integration/jira/development_panel.md4
-rw-r--r--jest.config.base.js5
-rw-r--r--lib/api/concerns/packages/npm_endpoints.rb6
-rw-r--r--lib/gitlab/git/blame_mode.rb37
-rw-r--r--locale/gitlab.pot28
-rw-r--r--spec/finders/group_members_finder_spec.rb89
-rw-r--r--spec/frontend/__helpers__/assert_props.js24
-rw-r--r--spec/frontend/blob/sketch/index_spec.js5
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js42
-rw-r--r--spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js14
-rw-r--r--spec/frontend/ci/runner/components/runner_type_badge_spec.js3
-rw-r--r--spec/frontend/diffs/components/app_spec.js35
-rw-r--r--spec/frontend/diffs/store/actions_spec.js91
-rw-r--r--spec/frontend/environment.js11
-rw-r--r--spec/frontend/monitoring/pages/dashboard_page_spec.js3
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js42
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js1
-rw-r--r--spec/frontend/releases/components/releases_sort_spec.js3
-rw-r--r--spec/frontend/super_sidebar/components/groups_list_spec.js2
-rw-r--r--spec/frontend/super_sidebar/components/projects_list_spec.js2
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js36
-rw-r--r--spec/frontend/vue_shared/components/slot_switch_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/split_button_spec.js5
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js3
-rw-r--r--spec/lib/gitlab/git/blame_mode_spec.rb84
-rw-r--r--spec/services/projects/blame_service_spec.rb3
-rw-r--r--spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb21
70 files changed, 861 insertions, 344 deletions
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index ad7f4552ae3..97bd121e1aa 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -131,17 +131,21 @@ retrieve-frontend-fixtures:
- .rails-cache
- .use-pg13
stage: fixtures
- needs: ["setup-test-env", "retrieve-tests-metadata"]
+ needs: ["setup-test-env", "retrieve-tests-metadata", "retrieve-frontend-fixtures"]
variables:
CRYSTALBALL: "false"
WEBPACK_VENDOR_DLL: "true"
script:
- source scripts/utils.sh
- source scripts/gitlab_component_helpers.sh
- - install_gitlab_gem
- - export_fixtures_sha_for_download
- |
- if check_fixtures_download; then
+ if [[ -d "tmp/tests/frontend" ]]; then
+ # Remove tmp/tests/frontend/ except on the first parallelized job so that depending
+ # jobs don't download the exact same artifact multiple times.
+ if [[ -n "${CI_NODE_INDEX}" ]] && [[ "${CI_NODE_INDEX}" -ne 1 ]]; then
+ echoinfo "INFO: Removing 'tmp/tests/frontend' as we're on node ${CI_NODE_INDEX}.";
+ rm -rf "tmp/tests/frontend";
+ fi
exit 0
else
echo "No frontend fixtures directory, generating frontend fixtures."
@@ -250,7 +254,7 @@ jest:
extends:
- .jest-base
- .frontend:rules:jest
- needs: ["rspec-all frontend_fixture", "retrieve-frontend-fixtures"]
+ needs: ["rspec-all frontend_fixture"]
artifacts:
name: coverage-frontend
expire_in: 31d
@@ -298,10 +302,7 @@ jest-integration:
- .frontend:rules:default-frontend-jobs
script:
- run_timed_command "yarn jest:integration --ci"
- needs:
- - job: "rspec-all frontend_fixture"
- - job: "retrieve-frontend-fixtures"
- - job: "graphql-schema-dump"
+ needs: ["rspec-all frontend_fixture", "graphql-schema-dump"]
coverage-frontend:
extends:
@@ -384,10 +385,7 @@ startup-css-check:
extends:
- .startup-css-check-base
- .frontend:rules:default-frontend-jobs
- needs:
- - job: "compile-test-assets"
- - job: "rspec-all frontend_fixture"
- - job: "retrieve-frontend-fixtures"
+ needs: ["compile-test-assets", "rspec-all frontend_fixture"]
startup-css-check as-if-foss:
extends:
@@ -414,7 +412,6 @@ compile-storybook:
needs:
- !reference [.compile-storybook-base, needs]
- job: "rspec-all frontend_fixture"
- - job: "retrieve-frontend-fixtures"
artifacts:
name: storybook
expire_in: 31d
diff --git a/.gitlab/ci/review-apps/dast.gitlab-ci.yml b/.gitlab/ci/review-apps/dast.gitlab-ci.yml
index 8f0c6b60190..d3019577ab4 100644
--- a/.gitlab/ci/review-apps/dast.gitlab-ci.yml
+++ b/.gitlab/ci/review-apps/dast.gitlab-ci.yml
@@ -5,14 +5,14 @@
extends:
- .reports:rules:schedule-dast
image:
- name: "${REGISTRY_HOST}/security-products/dast:$DAST_VERSION"
+ name: "${CI_TEMPLATE_REGISTRY_HOST}/security-products/dast:$DAST_VERSION"
resource_group: dast_scan
variables:
- DAST_USERNAME_FIELD: "user[login]"
- DAST_PASSWORD_FIELD: "user[password]"
- DAST_SUBMIT_FIELD: "name:button"
+ DAST_USERNAME_FIELD: "name:user[login]"
+ DAST_PASSWORD_FIELD: "name:user[password]"
+ DAST_SUBMIT_FIELD: "css:.js-sign-in-button"
DAST_FULL_SCAN_ENABLED: "true"
- DAST_VERSION: 2
+ DAST_VERSION: 3
GIT_STRATEGY: none
# -Xmx is used to set the JVM memory to 6GB to prevent DAST OutOfMemoryError.
DAST_ZAP_CLI_OPTIONS: "-Xmx6144m"
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 99bc3780b55..9b3db78724d 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -253,6 +253,9 @@ export default {
renderDiffFiles() {
return this.flatBlobsList.length > 0;
},
+ diffsIncomplete() {
+ return this.flatBlobsList.length !== this.diffFiles.length;
+ },
renderFileTree() {
return this.renderDiffFiles && this.showTreeList;
},
@@ -313,6 +316,11 @@ export default {
diffViewType() {
this.adjustView();
},
+ viewDiffsFileByFile(newViewFileByFile) {
+ if (!newViewFileByFile && this.diffsIncomplete && this.glFeatures.singleFileFileByFile) {
+ this.refetchDiffData({ refetchMeta: false });
+ }
+ },
shouldShow() {
// When the shouldShow property changed to true, the route is rendered for the first time
// and if we have the isLoading as true this means we didn't fetch the data
@@ -429,13 +437,15 @@ export default {
'setCodequalityEndpoint',
'fetchDiffFilesMeta',
'fetchDiffFilesBatch',
+ 'fetchFileByFile',
'fetchCoverageFiles',
'fetchCodequality',
+ 'rereadNoteHash',
'startRenderDiffsQueue',
'assignDiscussionsToDiff',
'setHighlightedRow',
'cacheTreeListWidth',
- 'scrollToFile',
+ 'goToFile',
'setShowTreeList',
'navigateToDiffFileIndex',
'setFileByFile',
@@ -448,16 +458,27 @@ export default {
subscribeToEvents() {
notesEventHub.$once('fetchDiffData', this.fetchData);
notesEventHub.$on('refetchDiffData', this.refetchDiffData);
+ if (this.glFeatures.singleFileFileByFile) {
+ diffsEventHub.$on('diffFilesModified', this.setDiscussions);
+ notesEventHub.$on('fetchedNotesData', this.rereadNoteHash);
+ }
},
unsubscribeFromEvents() {
+ if (this.glFeatures.singleFileFileByFile) {
+ notesEventHub.$off('fetchedNotesData', this.rereadNoteHash);
+ diffsEventHub.$off('diffFilesModified', this.setDiscussions);
+ }
notesEventHub.$off('refetchDiffData', this.refetchDiffData);
notesEventHub.$off('fetchDiffData', this.fetchData);
},
navigateToDiffFileNumber(number) {
- this.navigateToDiffFileIndex(number - 1);
+ this.navigateToDiffFileIndex({
+ index: number - 1,
+ singleFile: this.glFeatures.singleFileFileByFile,
+ });
},
- refetchDiffData() {
- this.fetchData(false);
+ refetchDiffData({ refetchMeta = true } = {}) {
+ this.fetchData({ toggleTree: false, fetchMeta: refetchMeta });
},
needsReload() {
return this.diffFiles.length && isSingleViewStyle(this.diffFiles[0]);
@@ -465,44 +486,52 @@ export default {
needsFirstLoad() {
return !this.diffFiles.length;
},
- fetchData(toggleTree = true) {
- this.fetchDiffFilesMeta()
- .then((data) => {
- let realSize = 0;
-
- if (data) {
- realSize = data.real_size;
- }
-
- this.diffFilesLength = parseInt(realSize, 10) || 0;
- if (toggleTree) {
- this.setTreeDisplay();
- }
-
- updateChangesTabCount({
- count: this.diffFilesLength,
+ fetchData({ toggleTree = true, fetchMeta = true } = {}) {
+ if (fetchMeta) {
+ this.fetchDiffFilesMeta()
+ .then((data) => {
+ let realSize = 0;
+
+ if (data) {
+ realSize = data.real_size;
+
+ if (this.viewDiffsFileByFile && this.glFeatures.singleFileFileByFile) {
+ this.fetchFileByFile();
+ }
+ }
+
+ this.diffFilesLength = parseInt(realSize, 10) || 0;
+ if (toggleTree) {
+ this.setTreeDisplay();
+ }
+
+ updateChangesTabCount({
+ count: this.diffFilesLength,
+ });
+ })
+ .catch(() => {
+ createAlert({
+ message: __('Something went wrong on our end. Please try again!'),
+ });
});
- })
- .catch(() => {
- createAlert({
- message: __('Something went wrong on our end. Please try again!'),
- });
- });
+ }
- this.fetchDiffFilesBatch()
- .then(() => {
- if (toggleTree) this.setTreeDisplay();
- // Guarantee the discussions are assigned after the batch finishes.
- // Just watching the length of the discussions or the diff files
- // isn't enough, because with split diff loading, neither will
- // change when loading the other half of the diff files.
- this.setDiscussions();
- })
- .catch(() => {
- createAlert({
- message: __('Something went wrong on our end. Please try again!'),
+ if (!this.viewDiffsFileByFile || !this.glFeatures.singleFileFileByFile) {
+ this.fetchDiffFilesBatch()
+ .then(() => {
+ if (toggleTree) this.setTreeDisplay();
+ // Guarantee the discussions are assigned after the batch finishes.
+ // Just watching the length of the discussions or the diff files
+ // isn't enough, because with split diff loading, neither will
+ // change when loading the other half of the diff files.
+ this.setDiscussions();
+ })
+ .catch(() => {
+ createAlert({
+ message: __('Something went wrong on our end. Please try again!'),
+ });
});
- });
+ }
if (this.endpointCoverage) {
this.fetchCoverageFiles();
@@ -578,7 +607,10 @@ export default {
jumpToFile(step) {
const targetIndex = this.currentDiffIndex + step;
if (targetIndex >= 0 && targetIndex < this.flatBlobsList.length) {
- this.scrollToFile({ path: this.flatBlobsList[targetIndex].path });
+ this.goToFile({
+ path: this.flatBlobsList[targetIndex].path,
+ singleFile: this.glFeatures.singleFileFileByFile,
+ });
}
},
setTreeDisplay() {
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index c19174dda8a..a58178eaef7 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -209,7 +209,11 @@ export default {
if (this.hasDiff) {
this.postRender();
- } else if (this.viewDiffsFileByFile && !this.isCollapsed) {
+ } else if (
+ this.viewDiffsFileByFile &&
+ !this.isCollapsed &&
+ !this.glFeatures.singleFileFileByFile
+ ) {
this.requestDiff();
}
diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue
index 2675099a2f5..4f1875e9175 100644
--- a/app/assets/javascripts/diffs/components/tree_list.vue
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -5,6 +5,7 @@ import micromatch from 'micromatch';
import { debounce } from 'lodash';
import { getModifierKey } from '~/constants';
import { s__, sprintf } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { RecycleScroller } from 'vendor/vue-virtual-scroller';
import DiffFileRow from './diff_file_row.vue';
@@ -19,6 +20,7 @@ export default {
DiffFileRow,
RecycleScroller,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
hideFileStats: {
type: Boolean,
@@ -105,7 +107,7 @@ export default {
this.resizeObserver.disconnect();
},
methods: {
- ...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']),
+ ...mapActions('diffs', ['toggleTreeOpen', 'goToFile']),
clearSearch() {
this.search = '';
},
@@ -175,7 +177,7 @@ export default {
:class="{ 'tree-list-parent': item.level > 0 }"
class="gl-relative"
@toggleTreeOpen="toggleTreeOpen"
- @clickFile="(path) => scrollToFile({ path })"
+ @clickFile="(path) => goToFile({ singleFile: glFeatures.singleFileFileByFile, path })"
/>
</template>
<template #after>
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index f6552d39193..a70c907314b 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -591,8 +591,8 @@ export const setCurrentFileHash = ({ commit }, hash) => {
commit(types.SET_CURRENT_DIFF_FILE, hash);
};
-export const goToFile = ({ state, commit, dispatch, getters }, { path }) => {
- if (!state.viewDiffsFileByFile) {
+export const goToFile = ({ state, commit, dispatch, getters }, { path, singleFile }) => {
+ if (!state.viewDiffsFileByFile || !singleFile) {
dispatch('scrollToFile', { path });
} else {
if (!state.treeEntries[path]) return;
@@ -600,9 +600,9 @@ export const goToFile = ({ state, commit, dispatch, getters }, { path }) => {
const { fileHash } = state.treeEntries[path];
commit(types.SET_CURRENT_DIFF_FILE, fileHash);
+ document.location.hash = fileHash;
if (!getters.isTreePathLoaded(path)) {
- document.location.hash = fileHash;
dispatch('fetchFileByFile')
.then(() => {
dispatch('scrollToFile', { path });
@@ -926,11 +926,18 @@ export const setCurrentDiffFileIdFromNote = ({ commit, getters, rootGetters }, n
}
};
-export const navigateToDiffFileIndex = ({ commit, getters }, index) => {
+export const navigateToDiffFileIndex = (
+ { state, getters, commit, dispatch },
+ { index, singleFile },
+) => {
const { fileHash } = getters.flatBlobsList[index];
document.location.hash = fileHash;
commit(types.SET_CURRENT_DIFF_FILE, fileHash);
+
+ if (state.viewDiffsFileByFile && singleFile) {
+ dispatch('fetchFileByFile');
+ }
};
export const setFileByFile = ({ state, commit }, { fileByFile }) => {
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue
index 5d77ff9dc0d..4e154870f55 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue
@@ -4,9 +4,10 @@ import { sprintf, n__, s__ } from '~/locale';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
+import { formatDate } from '~/lib/utils/datetime_utility';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import {
- UPDATED_AT,
+ CREATED_AT,
CLEANUP_UNSCHEDULED_TEXT,
CLEANUP_SCHEDULED_TEXT,
CLEANUP_ONGOING_TEXT,
@@ -65,11 +66,11 @@ export default {
visibilityIcon() {
return this.imageDetails?.project?.visibility === 'public' ? 'eye' : 'eye-slash';
},
- timeAgo() {
- return this.timeFormatted(this.imageDetails.updatedAt);
+ formattedCreatedAtDate() {
+ return formatDate(this.imageDetails.createdAt, 'mmm d, yyyy HH:MM', true);
},
- updatedText() {
- return sprintf(UPDATED_AT, { time: this.timeAgo });
+ createdText() {
+ return sprintf(CREATED_AT, { time: this.formattedCreatedAtDate });
},
tagCountText() {
if (this.$apollo.queries.containerRepository.loading) {
@@ -145,9 +146,9 @@ export default {
<template #metadata-updated>
<metadata-item
:icon="visibilityIcon"
- :text="updatedText"
+ :text="createdText"
size="xl"
- data-testid="updated-and-visibility"
+ data-testid="created-and-visibility"
/>
</template>
<template #right-actions>
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js
index 7bb69363743..7ac803a8ece 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js
@@ -65,7 +65,7 @@ export const MISSING_MANIFEST_WARNING_TOOLTIP = s__(
'ContainerRegistry|Invalid tag: missing manifest digest',
);
-export const UPDATED_AT = s__('ContainerRegistry|Last updated %{time}');
+export const CREATED_AT = s__('ContainerRegistry|Created %{time}');
export const NOT_AVAILABLE_TEXT = __('Not applicable.');
export const NOT_AVAILABLE_SIZE = __('0 bytes');
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql
index e2036d9e63d..eae663acb48 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql
@@ -7,7 +7,6 @@ query getContainerRepositoryDetails($id: ContainerRepositoryID!) {
location
canDelete
createdAt
- updatedAt
expirationPolicyStartedAt
expirationPolicyCleanupStatus
project {
diff --git a/app/assets/javascripts/super_sidebar/components/groups_list.vue b/app/assets/javascripts/super_sidebar/components/groups_list.vue
index 1360d58dc6c..eb256e4971b 100644
--- a/app/assets/javascripts/super_sidebar/components/groups_list.vue
+++ b/app/assets/javascripts/super_sidebar/components/groups_list.vue
@@ -45,7 +45,7 @@ export default {
},
},
i18n: {
- title: s__('Navigation|Frequent groups'),
+ title: s__('Navigation|Frequently visited groups'),
searchTitle: s__('Navigation|Groups'),
pristineText: s__('Navigation|Groups you visit often will appear here.'),
noResultsText: s__('Navigation|No group matches found'),
diff --git a/app/assets/javascripts/super_sidebar/components/projects_list.vue b/app/assets/javascripts/super_sidebar/components/projects_list.vue
index de22f5d9897..b7a29a78d5f 100644
--- a/app/assets/javascripts/super_sidebar/components/projects_list.vue
+++ b/app/assets/javascripts/super_sidebar/components/projects_list.vue
@@ -45,7 +45,7 @@ export default {
},
},
i18n: {
- title: s__('Navigation|Frequent projects'),
+ title: s__('Navigation|Frequently visited projects'),
searchTitle: s__('Navigation|Projects'),
pristineText: s__('Navigation|Projects you visit often will appear here.'),
noResultsText: s__('Navigation|No project matches found'),
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index aed2187a3e6..738305ad670 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -537,14 +537,13 @@ export default {
{{ workItemBreadcrumbReference }}
</li>
</ul>
- <work-item-type-icon
- v-else-if="!error"
- :work-item-icon-name="workItemIconName"
- :work-item-type="workItemType && workItemType.toUpperCase()"
- show-text
- class="gl-font-weight-bold gl-text-secondary gl-mr-auto"
- data-testid="work-item-type"
- />
+ <div v-else-if="!error" class="gl-mr-auto" data-testid="work-item-type">
+ <work-item-type-icon
+ :work-item-icon-name="workItemIconName"
+ :work-item-type="workItemType && workItemType.toUpperCase()"
+ />
+ {{ workItemBreadcrumbReference }}
+ </div>
<gl-loading-icon v-if="updateInProgress" :inline="true" class="gl-mr-3" />
<gl-badge
v-if="workItem.confidential"
diff --git a/app/assets/stylesheets/page_bundles/wiki.scss b/app/assets/stylesheets/page_bundles/wiki.scss
index 7625fbede3a..69a3ec94fda 100644
--- a/app/assets/stylesheets/page_bundles/wiki.scss
+++ b/app/assets/stylesheets/page_bundles/wiki.scss
@@ -216,17 +216,12 @@ ul.wiki-pages-list.content-list {
.drawio-editor {
position: fixed;
- top: calc(var(--header-height, 48px));
+ top: 0;
left: 0;
bottom: 0;
- width: 100%;
- height: calc(100% - var(--header-height, 48px));
+ width: 100vw;
+ height: 100vh;
border: 0;
z-index: 1100;
visibility: hidden;
}
-
-.with-performance-bar .drawio-editor {
- top: calc(var(--header-height, 48px) + 35px);
- height: calc(100% - var(--header-height, 48px) - 35px);
-}
diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb
index 75d7dfd5813..bd5701a3557 100644
--- a/app/controllers/projects/blame_controller.rb
+++ b/app/controllers/projects/blame_controller.rb
@@ -21,14 +21,12 @@ class Projects::BlameController < Projects::ApplicationController
load_environment
- blame_service = Projects::BlameService.new(@blob, @commit, blame_params)
+ @blame_mode = Gitlab::Git::BlameMode.new(@commit.project, blame_params)
+ blame_service = Projects::BlameService.new(@blob, @commit, @blame_mode, blame_params)
@blame = Gitlab::View::Presenter::Factory.new(blame_service.blame, project: @project, path: @path, page: blame_service.page).fabricate!
- @streaming_possible = blame_service.streaming_possible
-
- @streaming_enabled = blame_service.streaming_enabled
- @blame_pagination = blame_service.pagination unless @streaming_enabled
+ @blame_pagination = blame_service.pagination
@blame_per_page = blame_service.per_page
@@ -40,7 +38,8 @@ class Projects::BlameController < Projects::ApplicationController
load_environment
- blame_service = Projects::BlameService.new(@blob, @commit, blame_params)
+ @blame_mode = Gitlab::Git::BlameMode.new(@commit.project, blame_params)
+ blame_service = Projects::BlameService.new(@blob, @commit, @blame_mode, blame_params)
@blame = Gitlab::View::Presenter::Factory.new(blame_service.blame, project: @project, path: @path, page: blame_service.page).fabricate!
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index e174eadbeb4..73b8ca6aafb 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -41,6 +41,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:deprecate_vulnerabilities_feedback, @project)
push_frontend_feature_flag(:refactor_code_quality_inline_findings, project)
push_frontend_feature_flag(:moved_mr_sidebar, project)
+ push_frontend_feature_flag(:single_file_file_by_file, project)
push_frontend_feature_flag(:mr_experience_survey, project)
push_frontend_feature_flag(:realtime_mr_status_change, project)
push_frontend_feature_flag(:saved_replies, current_user)
diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb
index 05645dacab9..1025e0ebc9b 100644
--- a/app/finders/group_members_finder.rb
+++ b/app/finders/group_members_finder.rb
@@ -30,7 +30,11 @@ class GroupMembersFinder < UnionFinder
def execute(include_relations: DEFAULT_RELATIONS)
groups = groups_by_relations(include_relations)
- members = all_group_members(groups).distinct_on_user_with_max_access_level
+ shared_from_groups = if include_relations&.include?(:shared_from_groups)
+ Group.shared_into_ancestors(group).public_or_visible_to_user(user)
+ end
+
+ members = all_group_members(groups, shared_from_groups).distinct_on_user_with_max_access_level
filter_members(members)
end
@@ -47,9 +51,8 @@ class GroupMembersFinder < UnionFinder
related_groups << Group.by_id(group.id) if include_relations&.include?(:direct)
related_groups << group.ancestors if include_relations&.include?(:inherited)
related_groups << group.descendants if include_relations&.include?(:descendants)
- related_groups << Group.shared_into_ancestors(group).public_or_visible_to_user(user) if include_relations&.include?(:shared_from_groups)
- find_union(related_groups, Group)
+ related_groups
end
def filter_members(members)
@@ -78,12 +81,49 @@ class GroupMembersFinder < UnionFinder
group.members
end
- def all_group_members(groups)
- members_of_groups(groups).non_minimal_access
+ def all_group_members(groups, shared_from_groups)
+ members_of_groups(groups, shared_from_groups).non_minimal_access
+ end
+
+ def members_of_groups(groups, shared_from_groups)
+ if Feature.disabled?(:members_with_shared_group_access, @group.root_ancestor)
+ groups << shared_from_groups unless shared_from_groups.nil?
+ return GroupMember.non_request.of_groups(find_union(groups, Group))
+ end
+
+ members = GroupMember.non_request.of_groups(find_union(groups, Group))
+ return members if shared_from_groups.nil?
+
+ shared_members = GroupMember.non_request.of_groups(shared_from_groups)
+ select_attributes = GroupMember.attribute_names
+ members_shared_with_group_access = members_shared_with_group_access(shared_members, select_attributes)
+
+ # `members` and `members_shared_with_group_access` should have even select values
+ find_union([members.select(select_attributes), members_shared_with_group_access], GroupMember)
+ end
+
+ def members_shared_with_group_access(shared_members, select_attributes)
+ group_group_link_table = GroupGroupLink.arel_table
+ group_member_table = GroupMember.arel_table
+
+ member_columns = select_attributes.map do |column_name|
+ if column_name == 'access_level'
+ args = [group_group_link_table[:group_access], group_member_table[:access_level]]
+ smallest_value_arel(args, 'access_level')
+ else
+ group_member_table[column_name]
+ end
+ end
+
+ # rubocop:disable CodeReuse/ActiveRecord
+ shared_members
+ .joins("LEFT OUTER JOIN group_group_links ON members.source_id = group_group_links.shared_with_group_id")
+ .select(member_columns)
+ # rubocop:enable CodeReuse/ActiveRecord
end
- def members_of_groups(groups)
- GroupMember.non_request.of_groups(groups)
+ def smallest_value_arel(args, column_alias)
+ Arel::Nodes::As.new(Arel::Nodes::NamedFunction.new('LEAST', args), Arel::Nodes::SqlLiteral.new(column_alias))
end
def check_relation_arguments!(include_relations)
diff --git a/app/helpers/blame_helper.rb b/app/helpers/blame_helper.rb
index 92bee1795fa..4eda89e2af2 100644
--- a/app/helpers/blame_helper.rb
+++ b/app/helpers/blame_helper.rb
@@ -44,8 +44,8 @@ module BlameHelper
namespace_project_blame_page_url(namespace_id: project.namespace, project_id: project, id: id, streaming: true)
end
- def entire_blame_path(id, project, streaming_possible)
- params = streaming_possible ? { streaming: true } : { no_pagination: true }
+ def entire_blame_path(id, project, blame_mode)
+ params = blame_mode.streaming_supported? ? { streaming: true } : { no_pagination: true }
namespace_project_blame_path(namespace_id: project.namespace, project_id: project, id: id, **params)
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index a36d61378a0..3c52868eab4 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -260,6 +260,10 @@ module MergeRequestsHelper
Feature.enabled?(:moved_mr_sidebar, @project) && defined?(@merge_request)
end
+ def single_file_file_by_file?
+ Feature.enabled?(:single_file_file_by_file, @project)
+ end
+
def sticky_header_data
data = {
iid: @merge_request.iid,
diff --git a/app/models/packages/event.rb b/app/models/packages/event.rb
index a033f9a9393..d93c22adcda 100644
--- a/app/models/packages/event.rb
+++ b/app/models/packages/event.rb
@@ -18,6 +18,7 @@ module Packages
delete_tag
delete_tag_bulk
list_tags
+ create_tag
cli_metadata
pull_symbol_package
push_symbol_package
diff --git a/app/services/projects/blame_service.rb b/app/services/projects/blame_service.rb
index 1ea16040655..d4c01044828 100644
--- a/app/services/projects/blame_service.rb
+++ b/app/services/projects/blame_service.rb
@@ -8,23 +8,22 @@ module Projects
STREAMING_FIRST_PAGE_SIZE = 200
STREAMING_PER_PAGE = 2000
- def initialize(blob, commit, params)
+ def initialize(blob, commit, blame_mode, params)
@blob = blob
@commit = commit
- @streaming_enabled = streaming_state(params)
- @pagination_enabled = pagination_state(params)
+ @blame_mode = blame_mode
@page = extract_page(params)
@params = params
end
- attr_reader :page, :streaming_enabled
+ attr_reader :page
def blame
Gitlab::Blame.new(blob, commit, range: blame_range)
end
def pagination
- return unless pagination_enabled
+ return unless blame_mode.pagination?
Kaminari.paginate_array([], total_count: blob_lines_count, limit: per_page)
.tap { |pagination| pagination.max_paginates_per(per_page) }
@@ -32,12 +31,12 @@ module Projects
end
def per_page
- streaming_enabled ? STREAMING_PER_PAGE : PER_PAGE
+ blame_mode.streaming? ? STREAMING_PER_PAGE : PER_PAGE
end
def total_pages
total = (blob_lines_count.to_f / per_page).ceil
- return total unless streaming_enabled
+ return total unless blame_mode.streaming?
([blob_lines_count - STREAMING_FIRST_PAGE_SIZE, 0].max.to_f / per_page).ceil + 1
end
@@ -46,20 +45,16 @@ module Projects
[total_pages - 1, 0].max
end
- def streaming_possible
- Feature.enabled?(:blame_page_streaming, commit.project)
- end
-
private
- attr_reader :blob, :commit, :pagination_enabled
+ attr_reader :blob, :commit, :blame_mode
def blame_range
- return unless pagination_enabled || streaming_enabled
+ return if blame_mode.full?
first_line = (page - 1) * per_page + 1
- if streaming_enabled
+ if blame_mode.streaming?
return 1..STREAMING_FIRST_PAGE_SIZE if page == 1
first_line = STREAMING_FIRST_PAGE_SIZE + (page - 2) * per_page + 1
@@ -78,18 +73,6 @@ module Projects
page
end
- def streaming_state(params)
- return false unless streaming_possible
-
- Gitlab::Utils.to_boolean(params[:streaming], default: false)
- end
-
- def pagination_state(params)
- return false if Gitlab::Utils.to_boolean(params[:no_pagination], default: false)
-
- Feature.enabled?(:blame_page_pagination, commit.project)
- end
-
def overlimit?(page)
page > total_pages
end
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index 413a89f6399..689dcb3b2cb 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -2,7 +2,7 @@
- add_page_specific_style 'page_bundles/tree'
- blame_streaming_url = blame_pages_streaming_url(@id, @project)
-- if @streaming_enabled && total_extra_pages > 0
+- if @blame_mode.streaming? && total_extra_pages > 0
- content_for :startup_js do
= javascript_tag do
:plain
@@ -37,21 +37,21 @@
.blame-table-wrapper
= render partial: 'page'
- - if @streaming_enabled
+ - if @blame_mode.streaming?
#blame-stream-container.blame-stream-container
- - if @blame_pagination && @blame_pagination.total_pages > 1
+ - if @blame_mode.pagination? && @blame_pagination.total_pages > 1
.gl-display-flex.gl-justify-content-center.gl-flex-direction-column.gl-align-items-center.gl-p-3.gl-bg-gray-50.gl-border-t-solid.gl-border-t-1.gl-border-gray-100
- = render Pajamas::ButtonComponent.new(href: entire_blame_path(@id, @project, @streaming_possible), size: :small, button_options: { class: 'gl-mt-3' }) do |c|
+ = render Pajamas::ButtonComponent.new(href: entire_blame_path(@id, @project, @blame_mode), size: :small, button_options: { class: 'gl-mt-3' }) do |c|
= _('Show full blame')
- - if @streaming_enabled
+ - if @blame_mode.streaming?
#blame-stream-loading.blame-stream-loading
.gradient
= gl_loading_icon(size: 'sm')
%span.gl-mx-2
= _('Loading full blame...')
- - if @blame_pagination
+ - if @blame_mode.pagination?
= paginate(@blame_pagination, theme: "gitlab")
diff --git a/app/views/projects/merge_requests/_page.html.haml b/app/views/projects/merge_requests/_page.html.haml
index f042fd56132..c9c2d9ff13f 100644
--- a/app/views/projects/merge_requests/_page.html.haml
+++ b/app/views/projects/merge_requests/_page.html.haml
@@ -18,7 +18,7 @@
- add_page_specific_style 'page_bundles/ci_status'
- add_page_startup_api_call @endpoint_metadata_url
-- if mr_action == 'diffs'
+- if mr_action == 'diffs' && (!@file_by_file_default || !single_file_file_by_file?)
- add_page_startup_api_call @endpoint_diff_batch_url
.merge-request{ data: { mr_action: mr_action, url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project), lock_version: @merge_request.lock_version, diffs_batch_cache_key: @diffs_batch_cache_key } }
diff --git a/config/events/20230327141223_API__NpmProjectPackages_list_tags.yml b/config/events/20230327141223_API__NpmProjectPackages_list_tags.yml
new file mode 100644
index 00000000000..78021b29564
--- /dev/null
+++ b/config/events/20230327141223_API__NpmProjectPackages_list_tags.yml
@@ -0,0 +1,25 @@
+---
+description: List NPM project packages tags
+category: API::NpmProjectPackages
+action: list_tags
+label_description:
+property_description:
+value_description:
+extra_properties:
+identifiers:
+- project
+- user
+- namespace
+product_section: ops
+product_stage: package
+product_group: group::package registry
+product_category:
+milestone: "15.11"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/115545
+distributions:
+- ce
+- ee
+tiers:
+- free
+- premium
+- ultimate
diff --git a/config/events/20230327141524_API__NpmProjectPackages_create_tag.yml b/config/events/20230327141524_API__NpmProjectPackages_create_tag.yml
new file mode 100644
index 00000000000..6283880e81a
--- /dev/null
+++ b/config/events/20230327141524_API__NpmProjectPackages_create_tag.yml
@@ -0,0 +1,25 @@
+---
+description: Create NPM project packages tag
+category: API::NpmProjectPackages
+action: create_tag
+label_description:
+property_description:
+value_description:
+extra_properties:
+identifiers:
+- project
+- user
+- namespace
+product_section: ops
+product_stage: package
+product_group: group::package registry
+product_category:
+milestone: "15.11"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/115545
+distributions:
+- ce
+- ee
+tiers:
+- free
+- premium
+- ultimate
diff --git a/config/events/20230327141627_API__NpmProjectPackages_delete_tag.yml b/config/events/20230327141627_API__NpmProjectPackages_delete_tag.yml
new file mode 100644
index 00000000000..45a140a45aa
--- /dev/null
+++ b/config/events/20230327141627_API__NpmProjectPackages_delete_tag.yml
@@ -0,0 +1,25 @@
+---
+description: Delete NPM project packages tag
+category: API::NpmProjectPackages
+action: delete_tag
+label_description:
+property_description:
+value_description:
+extra_properties:
+identifiers:
+- project
+- user
+- namespace
+product_section: ops
+product_stage: package
+product_group: group::package registry
+product_category:
+milestone: "15.11"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/115545
+distributions:
+- ce
+- ee
+tiers:
+- free
+- premium
+- ultimate
diff --git a/config/events/20230327142004_API__NpmInstancePackages_list_tags.yml b/config/events/20230327142004_API__NpmInstancePackages_list_tags.yml
new file mode 100644
index 00000000000..aa63e85894d
--- /dev/null
+++ b/config/events/20230327142004_API__NpmInstancePackages_list_tags.yml
@@ -0,0 +1,25 @@
+---
+description: List NPM instance packages tags
+category: API::NpmInstancePackages
+action: list_tags
+label_description:
+property_description:
+value_description:
+extra_properties:
+identifiers:
+- project
+- user
+- namespace
+product_section: ops
+product_stage: package
+product_group: group::package registry
+product_category:
+milestone: "15.11"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/115545
+distributions:
+- ce
+- ee
+tiers:
+- free
+- premium
+- ultimate
diff --git a/config/events/20230327142151_API__NpmInstancePackages_create_tag.yml b/config/events/20230327142151_API__NpmInstancePackages_create_tag.yml
new file mode 100644
index 00000000000..f8de9d98e87
--- /dev/null
+++ b/config/events/20230327142151_API__NpmInstancePackages_create_tag.yml
@@ -0,0 +1,25 @@
+---
+description: Create NPM instance packages tag
+category: API::NpmInstancePackages
+action: create_tag
+label_description:
+property_description:
+value_description:
+extra_properties:
+identifiers:
+- project
+- user
+- namespace
+product_section: ops
+product_stage: package
+product_group: group::package registry
+product_category:
+milestone: "15.11"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/115545
+distributions:
+- ce
+- ee
+tiers:
+- free
+- premium
+- ultimate
diff --git a/config/events/20230327142237_API__NpmInstancePackages_delete_tag.yml b/config/events/20230327142237_API__NpmInstancePackages_delete_tag.yml
new file mode 100644
index 00000000000..aa78ff240f7
--- /dev/null
+++ b/config/events/20230327142237_API__NpmInstancePackages_delete_tag.yml
@@ -0,0 +1,25 @@
+---
+description: Delete NPM instance packages tag
+category: API::NpmInstancePackages
+action: delete_tag
+label_description:
+property_description:
+value_description:
+extra_properties:
+identifiers:
+- project
+- user
+- namespace
+product_section: ops
+product_stage: package
+product_group: group::package registry
+product_category:
+milestone: "15.11"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/115545
+distributions:
+- ce
+- ee
+tiers:
+- free
+- premium
+- ultimate
diff --git a/config/feature_flags/development/ai_experimentation_api.yml b/config/feature_flags/development/ai_experimentation_api.yml
new file mode 100644
index 00000000000..29a9fe4a181
--- /dev/null
+++ b/config/feature_flags/development/ai_experimentation_api.yml
@@ -0,0 +1,8 @@
+---
+name: ai_experimentation_api
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/117369
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/406754
+milestone: '15.11'
+type: development
+group: group::ai-enablement
+default_enabled: false
diff --git a/config/feature_flags/development/members_with_shared_group_access.yml b/config/feature_flags/development/members_with_shared_group_access.yml
new file mode 100644
index 00000000000..83d3ca62681
--- /dev/null
+++ b/config/feature_flags/development/members_with_shared_group_access.yml
@@ -0,0 +1,8 @@
+---
+name: members_with_shared_group_access
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/115346
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/398621
+milestone: '15.11'
+type: development
+group: group::tenant scale
+default_enabled: false
diff --git a/config/feature_flags/development/single_file_file_by_file.yml b/config/feature_flags/development/single_file_file_by_file.yml
new file mode 100644
index 00000000000..4efc81f18af
--- /dev/null
+++ b/config/feature_flags/development/single_file_file_by_file.yml
@@ -0,0 +1,8 @@
+---
+name: single_file_file_by_file
+introduced_by_url: 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111895'
+rollout_issue_url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/403571'
+milestone: '15.11'
+type: development
+group: group::code review
+default_enabled: false
diff --git a/doc/.vale/gitlab/Uppercase.yml b/doc/.vale/gitlab/Uppercase.yml
index 19e0fec6622..1948842d026 100644
--- a/doc/.vale/gitlab/Uppercase.yml
+++ b/doc/.vale/gitlab/Uppercase.yml
@@ -147,6 +147,7 @@ exceptions:
- NPM
- NTP
- OCI
+ - OIDC
- OKD
- OKR
- ONLY
diff --git a/doc/administration/geo/replication/single_sign_on.md b/doc/administration/geo/replication/single_sign_on.md
index fc2f23552db..55e77d5657c 100644
--- a/doc/administration/geo/replication/single_sign_on.md
+++ b/doc/administration/geo/replication/single_sign_on.md
@@ -112,6 +112,15 @@ After configuring your SAML IdP to allow the secondary site's SAML callback URL,
If you have configured SAML on the primary site correctly, then it should work on the secondary site without additional configuration.
+## OpenID Connect
+
+If you use an [OpenID Connect (OIDC)](../../auth/oidc.md) OmniAuth provider,
+in most cases, it should work without an issue:
+
+- **OIDC with Unified URL**: If you have configured OIDC on the primary site correctly, then it should work on the secondary site without additional configuration.
+- **OIDC with separate URL with proxying disabled**: If you have configured OIDC on the primary site correctly, then it should work on the secondary site without additional configuration.
+- **OIDC with separate URL with proxying enabled**: Geo with separate URL with proxying enabled does not support [OpenID Connect](../../auth/oidc.md). For more information, see [issue 396745](https://gitlab.com/gitlab-org/gitlab/-/issues/396745).
+
## LDAP
If you use LDAP on your **primary** site, you should also set up secondary LDAP servers on each **secondary** site. Otherwise, users cannot perform Git operations over HTTP(s) on the **secondary** site using HTTP basic authentication. However, users can still use Git with SSH and personal access tokens.
diff --git a/doc/ci/cloud_services/google_cloud/index.md b/doc/ci/cloud_services/google_cloud/index.md
index 97ea63f57e7..719b97c53d8 100644
--- a/doc/ci/cloud_services/google_cloud/index.md
+++ b/doc/ci/cloud_services/google_cloud/index.md
@@ -6,8 +6,9 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Configure OpenID Connect with GCP Workload Identity Federation **(FREE)**
-WARNING:
-The `CI_JOB_JWT_V2` variable is under development [(alpha)](../../../policy/alpha-beta-support.md#experiment) and is not yet suitable for production use.
+NOTE:
+`CI_JOB_JWT_V2` was [deprecated in GitLab 15.9](../../../update/deprecations.md#old-versions-of-json-web-tokens-are-deprecated)
+and is scheduled to be removed in GitLab 16.0. Use [ID tokens](../../yaml/index.md#id_tokens) instead.
This tutorial demonstrates authenticating to Google Cloud from a GitLab CI/CD job
using a JSON Web Token (JWT) token and Workload Identity Federation. This configuration
diff --git a/doc/ci/cloud_services/index.md b/doc/ci/cloud_services/index.md
index 88e7d88e22a..115488c3f73 100644
--- a/doc/ci/cloud_services/index.md
+++ b/doc/ci/cloud_services/index.md
@@ -8,7 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - `CI_JOB_JWT` variable for reading secrets from Vault [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/207125) in GitLab 12.10.
> - `CI_JOB_JWT_V2` variable to support additional OIDC providers [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/346737) in GitLab 14.7.
-> - [ID tokens](../yaml/index.md) to support any OIDC provider, including HashiCorp Vault, [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/356986) in GitLab 15.7.
+> - [ID tokens](../yaml/index.md#id_tokens) to support any OIDC provider, including HashiCorp Vault, [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/356986) in GitLab 15.7.
GitLab CI/CD supports [OpenID Connect (OIDC)](https://openid.net/connect/faq/) to
give your build and deployment jobs access to cloud credentials and services.
@@ -19,6 +19,10 @@ in the CI/CD job allowing you to follow a scalable and least-privilege security
In GitLab 15.6 and earlier, you must use `CI_JOB_JWT_V2` instead of an ID token,
but it is not customizable. In GitLab 14.6 an earlier you must use the `CI_JOB_JWT`, which has limited support.
+NOTE:
+`CI_JOB_JWT` and `CI_JOB_JWT_V2` were [deprecated in GitLab 15.9](../../update/deprecations.md#old-versions-of-json-web-tokens-are-deprecated)
+and are scheduled to be removed in GitLab 16.0. Use [ID tokens](../yaml/index.md#id_tokens) instead.
+
## Requirements
- Account on GitLab.
diff --git a/doc/ci/examples/authenticating-with-hashicorp-vault/index.md b/doc/ci/examples/authenticating-with-hashicorp-vault/index.md
index 6295c131fb9..f59bb8ed931 100644
--- a/doc/ci/examples/authenticating-with-hashicorp-vault/index.md
+++ b/doc/ci/examples/authenticating-with-hashicorp-vault/index.md
@@ -5,15 +5,13 @@ info: To determine the technical writer assigned to the Stage/Group associated w
type: tutorial
---
-# Authenticating and reading secrets with HashiCorp Vault **(PREMIUM)**
+# Authenticating and reading secrets with HashiCorp Vault (Deprecated) **(PREMIUM)**
This tutorial demonstrates how to authenticate, configure, and read secrets with HashiCorp's Vault from GitLab CI/CD.
NOTE:
-[GitLab Premium](https://about.gitlab.com/pricing/) supports read access to a
-HashiCorp Vault, and enables you to
-[use Vault secrets in a CI job](../../secrets/index.md#use-vault-secrets-in-a-ci-job).
-For more information, see [Using external secrets in CI](../../secrets/index.md).
+Authenticating with HashiCorp Vault by using `CI_JOB_JWT` was [deprecated in GitLab 15.9](../../../update/deprecations.md#old-versions-of-json-web-tokens-are-deprecated)
+and the token is scheduled to be removed in GitLab 16.0. Use [ID tokens to authenticate with HashiCorp Vault](../../secrets/id_token_authentication.md#automatic-id-token-authentication-with-hashicorp-vault) instead.
## Requirements
diff --git a/doc/ci/secrets/index.md b/doc/ci/secrets/index.md
index fa8fe2a36a8..6966a6baadf 100644
--- a/doc/ci/secrets/index.md
+++ b/doc/ci/secrets/index.md
@@ -23,14 +23,7 @@ GitLab has selected [Vault by HashiCorp](https://www.vaultproject.io) as the
first supported provider, and [KV-V2](https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v2)
as the first supported secrets engine.
-By default, GitLab authenticates using Vault's
-[JSON Web Token (JWT) authentication method](https://developer.hashicorp.com/vault/docs/auth/jwt#jwt-authentication), using
-the [JSON Web Token](https://gitlab.com/gitlab-org/gitlab/-/issues/207125) (`CI_JOB_JWT`).
-
-[ID tokens](../yaml/index.md#id_tokens) is the preferred secure way to authenticate with Vault,
-because ID tokens are defined per-job. GitLab can also authenticate with Vault by using the `CI_JOB_JWT`,
-but that token is provided to every job, which can be a security risk.
-
+Use [ID tokens](../yaml/index.md#id_tokens) to [authenticate with Vault](https://developer.hashicorp.com/vault/docs/auth/jwt#jwt-authentication).
The [Authenticating and Reading Secrets With HashiCorp Vault](../examples/authenticating-with-hashicorp-vault/index.md)
tutorial has more details about authenticating with ID tokens.
@@ -40,7 +33,7 @@ can use [use Vault secrets in a CI job](#use-vault-secrets-in-a-ci-job).
The flow for using GitLab with HashiCorp Vault
is summarized by this diagram:
-![Flow between GitLab and HashiCorp](../img/gitlab_vault_workflow_v13_4.png "How GitLab CI_JOB_JWT works with HashiCorp Vault")
+![Flow between GitLab and HashiCorp](../img/gitlab_vault_workflow_v13_4.png "How GitLab authenticates with HashiCorp Vault")
1. Configure your vault and secrets.
1. Generate your JWT and provide it to your CI job.
diff --git a/doc/ci/variables/index.md b/doc/ci/variables/index.md
index 515c7b13d4a..64a0f1038ad 100644
--- a/doc/ci/variables/index.md
+++ b/doc/ci/variables/index.md
@@ -469,7 +469,8 @@ To pass a job-created environment variable to other jobs:
- Values can be wrapped in quotes, but cannot contain newline characters.
1. Save the `.env` file as an [`artifacts:reports:dotenv`](../yaml/artifacts_reports.md#artifactsreportsdotenv)
artifact.
-1. Jobs in later stages can then [use the variable in scripts](#use-cicd-variables-in-job-scripts).
+1. Jobs in later stages can then [use the variable in scripts](#use-cicd-variables-in-job-scripts),
+ unless [jobs are configured not to receive `dotenv` variables](#control-which-jobs-receive-dotenv-variables).
For example:
diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md
index 5c298882d31..3de36479de7 100644
--- a/doc/ci/variables/predefined_variables.md
+++ b/doc/ci/variables/predefined_variables.md
@@ -68,9 +68,9 @@ as it can cause the pipeline to behave unexpectedly.
| `CI_HAS_OPEN_REQUIREMENTS` | 13.1 | all | Only available if the pipeline's project has an open [requirement](../../user/project/requirements/index.md). `true` when available. |
| `CI_JOB_ID` | 9.0 | all | The internal ID of the job, unique across all jobs in the GitLab instance. |
| `CI_JOB_IMAGE` | 12.9 | 12.9 | The name of the Docker image running the job. |
-| `CI_JOB_JWT` | 12.10 | all | A RS256 JSON web token to authenticate with third party systems that support JWT authentication, for example [HashiCorp's Vault](../secrets/index.md). |
-| `CI_JOB_JWT_V1` | 14.6 | all | The same value as `CI_JOB_JWT`. |
-| `CI_JOB_JWT_V2` | 14.6 | all | A newly formatted RS256 JSON web token to increase compatibility. Similar to `CI_JOB_JWT`, except the issuer (`iss`) claim is changed from `gitlab.com` to `https://gitlab.com`, `sub` has changed from `job_id` to a string that contains the project path, and an `aud` claim is added. Format is subject to change. Be aware, the `aud` field is a constant value. Trusting JWTs in multiple relying parties can lead to [one RP sending a JWT to another one and acting maliciously as a job](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72555#note_769112331). **Note:** The `CI_JOB_JWT_V2` variable is available for testing, but the full feature is planned to be generally available when [issue 360657](https://gitlab.com/gitlab-org/gitlab/-/issues/360657) is complete.|
+| `CI_JOB_JWT` (Deprecated) | 12.10 | all | A RS256 JSON web token to authenticate with third party systems that support JWT authentication, for example [HashiCorp's Vault](../secrets/index.md). [Deprecated in GitLab 15.9](../../update/deprecations.md#old-versions-of-json-web-tokens-are-deprecated) and scheduled to be removed in GitLab 16.0. Use [ID tokens](../yaml/index.md#id_tokens) instead. |
+| `CI_JOB_JWT_V1` (Deprecated) | 14.6 | all | The same value as `CI_JOB_JWT`. [Deprecated in GitLab 15.9](../../update/deprecations.md#old-versions-of-json-web-tokens-are-deprecated) and scheduled to be removed in GitLab 16.0. Use [ID tokens](../yaml/index.md#id_tokens) instead. |
+| `CI_JOB_JWT_V2` (Deprecated) | 14.6 | all | A newly formatted RS256 JSON web token to increase compatibility. Similar to `CI_JOB_JWT`, except the issuer (`iss`) claim is changed from `gitlab.com` to `https://gitlab.com`, `sub` has changed from `job_id` to a string that contains the project path, and an `aud` claim is added. The `aud` field is a constant value. Trusting JWTs in multiple relying parties can lead to [one RP sending a JWT to another one and acting maliciously as a job](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72555#note_769112331). [Deprecated in GitLab 15.9](../../update/deprecations.md#old-versions-of-json-web-tokens-are-deprecated) and scheduled to be removed in GitLab 16.0. Use [ID tokens](../yaml/index.md#id_tokens) instead. |
| `CI_JOB_MANUAL` | 8.12 | all | Only available if the job was started manually. `true` when available. |
| `CI_JOB_NAME` | 9.0 | 0.5 | The name of the job. |
| `CI_JOB_NAME_SLUG` | 15.4 | all | `CI_JOB_NAME_SLUG` in lowercase, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. No leading / trailing `-`. Use in paths. |
diff --git a/doc/development/application_secrets.md b/doc/development/application_secrets.md
index 526cc6c3f61..e640a94f0a6 100644
--- a/doc/development/application_secrets.md
+++ b/doc/development/application_secrets.md
@@ -17,7 +17,7 @@ This page is a development guide for application secrets.
|`db_key_base` | The base key to encrypt the data for `attr_encrypted` columns |
|`openid_connect_signing_key` | The signing key for OpenID Connect |
| `encrypted_settings_key_base` | The base key to encrypt settings files with |
-| `ci_jwt_signing_key` | The base key for encrypting the `CI_JOB_JWT` and `CI_JOB_JWT_V2` predefined CI/CD variables |
+| `ci_jwt_signing_key` | The base key for encrypting the `CI_JOB_JWT` and `CI_JOB_JWT_V2` predefined CI/CD variables. `CI_JOB_JWT` and `CI_JOB_JWT_V2` were [deprecated in GitLab 15.9](../update/deprecations.md#old-versions-of-json-web-tokens-are-deprecated) and are scheduled to be removed in GitLab 16.0. |
## Where the secrets are stored
diff --git a/doc/development/fe_guide/style/vue.md b/doc/development/fe_guide/style/vue.md
index e9d2a724d9d..a3ab1c1af30 100644
--- a/doc/development/fe_guide/style/vue.md
+++ b/doc/development/fe_guide/style/vue.md
@@ -650,6 +650,18 @@ over [`expect.objectContaining`](https://jestjs.io/docs/expect#expectobjectconta
});
```
+### Testing props validation
+
+1. When checking component props use `assertProps` helper. Props validation failures will be thrown as errors
+
+```javascript
+import { assertProps } from 'helpers/assert_props'
+
+// ...
+
+expect(() => assertProps(SomeComponent, { invalidPropValue: '1', someOtherProp: 2 })).toThrow()
+```
+
## The JavaScript/Vue Accord
The goal of this accord is to make sure we are all on the same page.
diff --git a/doc/integration/jira/configure.md b/doc/integration/jira/configure.md
index d6732321e7c..f1f7a34ac94 100644
--- a/doc/integration/jira/configure.md
+++ b/doc/integration/jira/configure.md
@@ -8,10 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
The Jira integration connects one or more GitLab projects to a Jira instance. You can host the Jira instance yourself or in [Atlassian Cloud](https://www.atlassian.com/migration/assess/why-cloud). The supported Jira versions are `6.x`, `7.x`, `8.x`, and `9.x`.
-You can set up the [Jira integration](index.md#jira-integration)
-by configuring your project settings in GitLab.
-You can also configure these settings at a [group level](../../user/admin_area/settings/project_integration_management.md#manage-group-level-default-settings-for-a-project-integration),
-and for self-managed GitLab, at an [instance level](../../user/admin_area/settings/project_integration_management.md#manage-instance-level-default-settings-for-a-project-integration).
+## Configure the integration
Prerequisites:
@@ -22,9 +19,13 @@ Prerequisites:
and the email address you used to create the token.
See [authentication in Jira](index.md#authentication-in-jira).
-## Configure a project
+You can enable the Jira integration by configuring your project settings in GitLab.
+You can also configure these settings at the:
-To configure your project:
+- [Group level](../../user/admin_area/settings/project_integration_management.md#manage-group-level-default-settings-for-a-project-integration)
+- [Instance level](../../user/admin_area/settings/project_integration_management.md#manage-instance-level-default-settings-for-a-project-integration) for self-managed GitLab
+
+To configure your project settings in GitLab:
1. On the top bar, select **Main menu > Projects** and find your project.
1. On the left sidebar, select **Settings > Integrations**.
diff --git a/doc/integration/jira/development_panel.md b/doc/integration/jira/development_panel.md
index 370b9178441..69394e2d445 100644
--- a/doc/integration/jira/development_panel.md
+++ b/doc/integration/jira/development_panel.md
@@ -8,8 +8,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/233149) from GitLab Premium to GitLab Free in 13.4.
-The Jira development panel connects all GitLab projects in a group or personal namespace.
-You can view GitLab activity from the Jira development panel.
+The Jira development panel connects all GitLab projects in a group or personal namespace
+where you can view GitLab activity.
When you're in GitLab, you can refer to a Jira issue by ID.
The [activity for that issue](https://support.atlassian.com/jira-software-cloud/docs/view-development-information-for-an-issue/)
diff --git a/jest.config.base.js b/jest.config.base.js
index 8c063e7173f..d9848b59b56 100644
--- a/jest.config.base.js
+++ b/jest.config.base.js
@@ -90,7 +90,8 @@ module.exports = (path, options = {}) => {
const TEST_FIXTURES_PATTERN = 'test_fixtures(/.*)$';
const TEST_FIXTURES_HOME = '/tmp/tests/frontend/fixtures';
const TEST_FIXTURES_HOME_EE = '/tmp/tests/frontend/fixtures-ee';
- const TEST_FIXTURES_RAW_LOADER_PATTERN = `${TEST_FIXTURES_HOME}.*\\.html$`;
+ const TEST_FIXTURES_STATIC_HOME = '/spec/frontend/fixtures/static';
+ const TEST_FIXTURES_RAW_LOADER_PATTERN = `(${TEST_FIXTURES_HOME}|${TEST_FIXTURES_STATIC_HOME}).*\\.html$`;
const moduleNameMapper = {
'^~(/.*)\\?(worker|raw)$': '<rootDir>/app/assets/javascripts$1',
@@ -107,7 +108,7 @@ module.exports = (path, options = {}) => {
'^helpers(/.*)$': '<rootDir>/spec/frontend/__helpers__$1',
'^vendor(/.*)$': '<rootDir>/vendor/assets/javascripts$1',
[TEST_FIXTURES_PATTERN]: `<rootDir>${TEST_FIXTURES_HOME}$1`,
- '^test_fixtures_static(/.*)$': '<rootDir>/spec/frontend/fixtures/static$1',
+ '^test_fixtures_static(/.*)$': `<rootDir>${TEST_FIXTURES_STATIC_HOME}$1`,
'\\.(jpg|jpeg|png|svg|css)$': '<rootDir>/spec/frontend/__mocks__/file_mock.js',
'\\.svg\\?url$': '<rootDir>/spec/frontend/__mocks__/file_mock.js',
'^public(/.*)$': '<rootDir>/public$1',
diff --git a/lib/api/concerns/packages/npm_endpoints.rb b/lib/api/concerns/packages/npm_endpoints.rb
index 3e7d5ac9d0c..afbde296161 100644
--- a/lib/api/concerns/packages/npm_endpoints.rb
+++ b/lib/api/concerns/packages/npm_endpoints.rb
@@ -77,6 +77,8 @@ module API
not_found! if packages.empty?
+ track_package_event(:list_tags, :npm, project: project, namespace: project.namespace)
+
metadata = generate_metadata_service(packages).execute(only_dist_tags: true)
present ::Packages::Npm::PackagePresenter.new(metadata),
with: ::API::Entities::NpmPackageTag
@@ -113,6 +115,8 @@ module API
.find_by_version(version)
not_found!('Package') unless package
+ track_package_event(:create_tag, :npm, project: project, namespace: project.namespace)
+
::Packages::Npm::CreateTagService.new(package, tag).execute
no_content!
@@ -145,6 +149,8 @@ module API
not_found!('Package tag') unless package_tag
+ track_package_event(:delete_tag, :npm, project: project, namespace: project.namespace)
+
::Packages::RemoveTagService.new(package_tag).execute
no_content!
diff --git a/lib/gitlab/git/blame_mode.rb b/lib/gitlab/git/blame_mode.rb
new file mode 100644
index 00000000000..d8fc8fece06
--- /dev/null
+++ b/lib/gitlab/git/blame_mode.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Git
+ class BlameMode
+ def initialize(project, params)
+ @project = project
+ @params = params
+ end
+
+ def streaming_supported?
+ Feature.enabled?(:blame_page_streaming, project)
+ end
+
+ def streaming?
+ return false unless streaming_supported?
+
+ Gitlab::Utils.to_boolean(params[:streaming], default: false)
+ end
+
+ def pagination?
+ return false if streaming?
+ return false if Gitlab::Utils.to_boolean(params[:no_pagination], default: false)
+
+ Feature.enabled?(:blame_page_pagination, project)
+ end
+
+ def full?
+ !streaming? && !pagination?
+ end
+
+ private
+
+ attr_reader :project, :params
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 91c9f4af5ba..7023c8a10e3 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1839,6 +1839,21 @@ msgstr ""
msgid "ACTION REQUIRED: Something went wrong while obtaining the Let's Encrypt certificate for GitLab Pages domain '%{domain}'"
msgstr ""
+msgid "AI|Close the Code Explanation"
+msgstr ""
+
+msgid "AI|Code Explanation"
+msgstr ""
+
+msgid "AI|Explain the code from %{filePath} in human understandable language presented in Markdown format. In the response add neither original code snippet nor any title. `%{text}`"
+msgstr ""
+
+msgid "AI|The container element wasn't found, stopping AI Genie."
+msgstr ""
+
+msgid "AI|What does the selected code mean?"
+msgstr ""
+
msgid "API"
msgstr ""
@@ -11340,6 +11355,9 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
+msgid "ContainerRegistry|Created %{time}"
+msgstr ""
+
msgid "ContainerRegistry|Delete image repository?"
msgstr ""
@@ -11397,9 +11415,6 @@ msgstr ""
msgid "ContainerRegistry|Keep these tags"
msgstr ""
-msgid "ContainerRegistry|Last updated %{time}"
-msgstr ""
-
msgid "ContainerRegistry|Login"
msgstr ""
@@ -28669,10 +28684,10 @@ msgstr ""
msgid "Navigation|Explore"
msgstr ""
-msgid "Navigation|Frequent groups"
+msgid "Navigation|Frequently visited groups"
msgstr ""
-msgid "Navigation|Frequent projects"
+msgid "Navigation|Frequently visited projects"
msgstr ""
msgid "Navigation|Groups"
@@ -53075,6 +53090,9 @@ msgstr ""
msgid "projects"
msgstr ""
+msgid "random"
+msgstr ""
+
msgid "reCAPTCHA"
msgstr ""
diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb
index 5d748f71816..4fc49289fa4 100644
--- a/spec/finders/group_members_finder_spec.rb
+++ b/spec/finders/group_members_finder_spec.rb
@@ -56,44 +56,67 @@ RSpec.describe GroupMembersFinder, '#execute', feature_category: :subgroups do
}
end
- it 'raises an error if a non-supported relation type is used' do
- expect do
- described_class.new(group).execute(include_relations: [:direct, :invalid_relation_type])
- end.to raise_error(ArgumentError, "invalid_relation_type is not a valid relation type. Valid relation types are direct, inherited, descendants, shared_from_groups.")
- end
+ shared_examples 'member relations' do
+ it 'raises an error if a non-supported relation type is used' do
+ expect do
+ described_class.new(group).execute(include_relations: [:direct, :invalid_relation_type])
+ end.to raise_error(ArgumentError, "invalid_relation_type is not a valid relation type. Valid relation types are direct, inherited, descendants, shared_from_groups.")
+ end
+
+ using RSpec::Parameterized::TableSyntax
+
+ where(:subject_relations, :subject_group, :expected_members) do
+ [] | :group | []
+ GroupMembersFinder::DEFAULT_RELATIONS | :group | [:user1_group, :user2_group, :user3_group, :user4_group]
+ [:direct] | :group | [:user1_group, :user2_group, :user3_group, :user4_group]
+ [:inherited] | :group | []
+ [:descendants] | :group | [:user1_sub_sub_group, :user2_sub_group, :user3_sub_group, :user4_sub_group]
+ [:shared_from_groups] | :group | [:user1_public_shared_group, :user2_public_shared_group, :user3_public_shared_group, :user4_public_shared_group]
+ [:direct, :inherited, :descendants, :shared_from_groups] | :group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_public_shared_group]
+ [] | :sub_group | []
+ GroupMembersFinder::DEFAULT_RELATIONS | :sub_group | [:user1_sub_group, :user2_group, :user3_sub_group, :user4_group]
+ [:direct] | :sub_group | [:user1_sub_group, :user2_sub_group, :user3_sub_group, :user4_sub_group]
+ [:inherited] | :sub_group | [:user1_group, :user2_group, :user3_group, :user4_group]
+ [:descendants] | :sub_group | [:user1_sub_sub_group, :user2_sub_sub_group, :user3_sub_sub_group, :user4_sub_sub_group]
+ [:shared_from_groups] | :sub_group | [:user1_public_shared_group, :user2_public_shared_group, :user3_public_shared_group, :user4_public_shared_group]
+ [:direct, :inherited, :descendants, :shared_from_groups] | :sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_public_shared_group]
+ [] | :sub_sub_group | []
+ GroupMembersFinder::DEFAULT_RELATIONS | :sub_sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group]
+ [:direct] | :sub_sub_group | [:user1_sub_sub_group, :user2_sub_sub_group, :user3_sub_sub_group, :user4_sub_sub_group]
+ [:inherited] | :sub_sub_group | [:user1_sub_group, :user2_group, :user3_sub_group, :user4_group]
+ [:descendants] | :sub_sub_group | []
+ [:shared_from_groups] | :sub_sub_group | [:user1_public_shared_group, :user2_public_shared_group, :user3_public_shared_group, :user4_public_shared_group]
+ [:direct, :inherited, :descendants, :shared_from_groups] | :sub_sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_public_shared_group]
+ end
+
+ with_them do
+ it 'returns correct members' do
+ result = described_class.new(groups[subject_group]).execute(include_relations: subject_relations)
- using RSpec::Parameterized::TableSyntax
-
- where(:subject_relations, :subject_group, :expected_members) do
- [] | :group | []
- GroupMembersFinder::DEFAULT_RELATIONS | :group | [:user1_group, :user2_group, :user3_group, :user4_group]
- [:direct] | :group | [:user1_group, :user2_group, :user3_group, :user4_group]
- [:inherited] | :group | []
- [:descendants] | :group | [:user1_sub_sub_group, :user2_sub_group, :user3_sub_group, :user4_sub_group]
- [:shared_from_groups] | :group | [:user1_public_shared_group, :user2_public_shared_group, :user3_public_shared_group, :user4_public_shared_group]
- [:direct, :inherited, :descendants, :shared_from_groups] | :group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_public_shared_group]
- [] | :sub_group | []
- GroupMembersFinder::DEFAULT_RELATIONS | :sub_group | [:user1_sub_group, :user2_group, :user3_sub_group, :user4_group]
- [:direct] | :sub_group | [:user1_sub_group, :user2_sub_group, :user3_sub_group, :user4_sub_group]
- [:inherited] | :sub_group | [:user1_group, :user2_group, :user3_group, :user4_group]
- [:descendants] | :sub_group | [:user1_sub_sub_group, :user2_sub_sub_group, :user3_sub_sub_group, :user4_sub_sub_group]
- [:shared_from_groups] | :sub_group | [:user1_public_shared_group, :user2_public_shared_group, :user3_public_shared_group, :user4_public_shared_group]
- [:direct, :inherited, :descendants, :shared_from_groups] | :sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_public_shared_group]
- [] | :sub_sub_group | []
- GroupMembersFinder::DEFAULT_RELATIONS | :sub_sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group]
- [:direct] | :sub_sub_group | [:user1_sub_sub_group, :user2_sub_sub_group, :user3_sub_sub_group, :user4_sub_sub_group]
- [:inherited] | :sub_sub_group | [:user1_sub_group, :user2_group, :user3_sub_group, :user4_group]
- [:descendants] | :sub_sub_group | []
- [:shared_from_groups] | :sub_sub_group | [:user1_public_shared_group, :user2_public_shared_group, :user3_public_shared_group, :user4_public_shared_group]
- [:direct, :inherited, :descendants, :shared_from_groups] | :sub_sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_public_shared_group]
+ expect(result.to_a).to match_array(expected_members.map { |name| members[name] })
+ end
+ end
end
- with_them do
- it 'returns correct members' do
- result = described_class.new(groups[subject_group]).execute(include_relations: subject_relations)
+ it_behaves_like 'member relations'
+
+ it 'returns the correct access level of the members shared through group sharing' do
+ shared_members_access = described_class
+ .new(groups[:group])
+ .execute(include_relations: [:shared_from_groups])
+ .to_a
+ .map(&:access_level)
+
+ correct_access_levels = ([Gitlab::Access::DEVELOPER] * 3) << Gitlab::Access::REPORTER
+ expect(shared_members_access).to match_array(correct_access_levels)
+ end
- expect(result.to_a).to match_array(expected_members.map { |name| members[name] })
+ context 'when members_with_shared_group_access feature flag is disabled' do
+ before do
+ stub_feature_flags(members_with_shared_group_access: false)
end
+
+ it_behaves_like 'member relations'
end
end
diff --git a/spec/frontend/__helpers__/assert_props.js b/spec/frontend/__helpers__/assert_props.js
new file mode 100644
index 00000000000..3e372454bf5
--- /dev/null
+++ b/spec/frontend/__helpers__/assert_props.js
@@ -0,0 +1,24 @@
+import { mount } from '@vue/test-utils';
+import { ErrorWithStack } from 'jest-util';
+
+export function assertProps(Component, props, extraMountArgs = {}) {
+ const originalConsoleError = global.console.error;
+ global.console.error = function error(...args) {
+ throw new ErrorWithStack(
+ `Unexpected call of console.error() with:\n\n${args.join(', ')}`,
+ this.error,
+ );
+ };
+ const ComponentWithoutRenderFn = {
+ ...Component,
+ render() {
+ return '';
+ },
+ };
+
+ try {
+ mount(ComponentWithoutRenderFn, { propsData: props, ...extraMountArgs });
+ } finally {
+ global.console.error = originalConsoleError;
+ }
+}
diff --git a/spec/frontend/blob/sketch/index_spec.js b/spec/frontend/blob/sketch/index_spec.js
index 4b6cb79791c..64b6152a07d 100644
--- a/spec/frontend/blob/sketch/index_spec.js
+++ b/spec/frontend/blob/sketch/index_spec.js
@@ -1,10 +1,11 @@
import SketchLoader from '~/blob/sketch';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
+import htmlSketchViewer from 'test_fixtures_static/sketch_viewer.html';
describe('Sketch viewer', () => {
beforeEach(() => {
- loadHTMLFixture('static/sketch_viewer.html');
+ setHTMLFixture(htmlSketchViewer);
});
afterEach(() => {
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
index 4107b08e9a4..c7bcace3883 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlLoadingIcon, GlTable } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { assertProps } from 'helpers/assert_props';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
@@ -593,19 +594,14 @@ describe('Ci Variable Shared Component', () => {
}
});
- it('will not mount component with wrong data', async () => {
- try {
- await createComponentWithApollo({
- customHandlers: [[getGroupVariables, mockVariables]],
- props: { ...createGroupProps(), queryData: { wrongKey: {} } },
- provide: pagesFeatureFlagProvide,
- });
- } catch (e) {
- error = e;
- } finally {
- expect(wrapper.exists()).toBe(false);
- expect(error.toString()).toContain('custom validator check failed for prop');
- }
+ it('report custom validator error on wrong data', () => {
+ expect(() =>
+ assertProps(
+ ciVariableShared,
+ { ...defaultProps, ...createGroupProps(), queryData: { wrongKey: {} } },
+ { provide: mockProvide },
+ ),
+ ).toThrow('custom validator check failed for prop');
});
});
@@ -630,18 +626,14 @@ describe('Ci Variable Shared Component', () => {
}
});
- it('will not mount component with wrong data', async () => {
- try {
- await createComponentWithApollo({
- props: { ...createGroupProps(), mutationData: { wrongKey: {} } },
- provide: pagesFeatureFlagProvide,
- });
- } catch (e) {
- error = e;
- } finally {
- expect(wrapper.exists()).toBe(false);
- expect(error.toString()).toContain('custom validator check failed for prop');
- }
+ it('report custom validator error on wrong data', async () => {
+ expect(() =>
+ assertProps(
+ ciVariableShared,
+ { ...defaultProps, ...createGroupProps(), mutationData: { wrongKey: {} } },
+ { provide: { ...mockProvide, ...pagesFeatureFlagProvide } },
+ ),
+ ).toThrow('custom validator check failed for prop');
});
});
});
diff --git a/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js b/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js
index ac84c7898bf..7572122a5f3 100644
--- a/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js
+++ b/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js
@@ -1,5 +1,6 @@
import { GlFilteredSearch, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { assertProps } from 'helpers/assert_props';
import RunnerFilteredSearchBar from '~/ci/runner/components/runner_filtered_search_bar.vue';
import { statusTokenConfig } from '~/ci/runner/components/search_tokens/status_token_config';
import TagToken from '~/ci/runner/components/search_tokens/tag_token.vue';
@@ -43,12 +44,12 @@ describe('RunnerList', () => {
expect(inputs[inputs.length - 1][0]).toEqual(value);
};
+ const defaultProps = { namespace: 'runners', tokens: [], value: mockSearch };
+
const createComponent = ({ props = {}, options = {} } = {}) => {
wrapper = shallowMountExtended(RunnerFilteredSearchBar, {
propsData: {
- namespace: 'runners',
- tokens: [],
- value: mockSearch,
+ ...defaultProps,
...props,
},
stubs: {
@@ -109,11 +110,14 @@ describe('RunnerList', () => {
it('fails validation for v-model with the wrong shape', () => {
expect(() => {
- createComponent({ props: { value: { filters: 'wrong_filters', sort: 'sort' } } });
+ assertProps(RunnerFilteredSearchBar, {
+ ...defaultProps,
+ value: { filters: 'wrong_filters', sort: 'sort' },
+ });
}).toThrow('Invalid prop: custom validator check failed');
expect(() => {
- createComponent({ props: { value: { sort: 'sort' } } });
+ assertProps(RunnerFilteredSearchBar, { ...defaultProps, value: { sort: 'sort' } });
}).toThrow('Invalid prop: custom validator check failed');
});
diff --git a/spec/frontend/ci/runner/components/runner_type_badge_spec.js b/spec/frontend/ci/runner/components/runner_type_badge_spec.js
index 7a0fb6f69ea..f7ecd108967 100644
--- a/spec/frontend/ci/runner/components/runner_type_badge_spec.js
+++ b/spec/frontend/ci/runner/components/runner_type_badge_spec.js
@@ -2,6 +2,7 @@ import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import RunnerTypeBadge from '~/ci/runner/components/runner_type_badge.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { assertProps } from 'helpers/assert_props';
import {
INSTANCE_TYPE,
GROUP_TYPE,
@@ -50,7 +51,7 @@ describe('RunnerTypeBadge', () => {
it('validation fails for an incorrect type', () => {
expect(() => {
- createComponent({ props: { type: 'AN_UNKNOWN_VALUE' } });
+ assertProps(RunnerTypeBadge, { type: 'AN_UNKNOWN_VALUE' });
}).toThrow();
});
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index 545ba4a0e04..ab01c3e528e 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -715,19 +715,27 @@ describe('diffs/components/app', () => {
});
it.each`
- currentDiffFileId | targetFile
- ${'123'} | ${2}
- ${'312'} | ${1}
+ currentDiffFileId | targetFile | newFileByFile
+ ${'123'} | ${2} | ${false}
+ ${'312'} | ${1} | ${true}
`(
'calls navigateToDiffFileIndex with $index when $link is clicked',
- async ({ currentDiffFileId, targetFile }) => {
- createComponent({ fileByFileUserPreference: true }, ({ state }) => {
- state.diffs.treeEntries = {
- 123: { type: 'blob', fileHash: '123' },
- 312: { type: 'blob', fileHash: '312' },
- };
- state.diffs.currentDiffFileId = currentDiffFileId;
- });
+ async ({ currentDiffFileId, targetFile, newFileByFile }) => {
+ createComponent(
+ { fileByFileUserPreference: true },
+ ({ state }) => {
+ state.diffs.treeEntries = {
+ 123: { type: 'blob', fileHash: '123', filePaths: { old: '1234', new: '123' } },
+ 312: { type: 'blob', fileHash: '312', filePaths: { old: '3124', new: '312' } },
+ };
+ state.diffs.currentDiffFileId = currentDiffFileId;
+ },
+ {
+ glFeatures: {
+ singleFileFileByFile: newFileByFile,
+ },
+ },
+ );
await nextTick();
@@ -737,7 +745,10 @@ describe('diffs/components/app', () => {
await nextTick();
- expect(wrapper.vm.navigateToDiffFileIndex).toHaveBeenCalledWith(targetFile - 1);
+ expect(wrapper.vm.navigateToDiffFileIndex).toHaveBeenCalledWith({
+ index: targetFile - 1,
+ singleFile: newFileByFile,
+ });
},
);
});
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index b988472f947..f3581c3dd74 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -1026,50 +1026,67 @@ describe('DiffsStoreActions', () => {
});
describe('when the app is in fileByFile mode', () => {
- it('commits SET_CURRENT_DIFF_FILE', () => {
- diffActions.goToFile({ state, commit, dispatch, getters }, file);
+ describe('when the singleFileFileByFile feature flag is enabled', () => {
+ it('commits SET_CURRENT_DIFF_FILE', () => {
+ diffActions.goToFile(
+ { state, commit, dispatch, getters },
+ { path: file.path, singleFile: true },
+ );
- expect(commit).toHaveBeenCalledWith(types.SET_CURRENT_DIFF_FILE, fileHash);
- });
+ expect(commit).toHaveBeenCalledWith(types.SET_CURRENT_DIFF_FILE, fileHash);
+ });
- it('does nothing more if the path has already been loaded', () => {
- getters.isTreePathLoaded = () => true;
+ it('does nothing more if the path has already been loaded', () => {
+ getters.isTreePathLoaded = () => true;
- diffActions.goToFile({ state, dispatch, getters, commit }, file);
+ diffActions.goToFile(
+ { state, dispatch, getters, commit },
+ { path: file.path, singleFile: true },
+ );
- expect(commit).toHaveBeenCalledWith(types.SET_CURRENT_DIFF_FILE, fileHash);
- expect(dispatch).toHaveBeenCalledTimes(0);
- });
+ expect(commit).toHaveBeenCalledWith(types.SET_CURRENT_DIFF_FILE, fileHash);
+ expect(dispatch).toHaveBeenCalledTimes(0);
+ });
- describe('when the tree entry has not been loaded', () => {
- it('updates location hash', () => {
- diffActions.goToFile({ state, commit, getters, dispatch }, file);
+ describe('when the tree entry has not been loaded', () => {
+ it('updates location hash', () => {
+ diffActions.goToFile(
+ { state, commit, getters, dispatch },
+ { path: file.path, singleFile: true },
+ );
- expect(document.location.hash).toBe('#test');
- });
+ expect(document.location.hash).toBe('#test');
+ });
- it('loads the file and then scrolls to it', async () => {
- diffActions.goToFile({ state, commit, getters, dispatch }, file);
+ it('loads the file and then scrolls to it', async () => {
+ diffActions.goToFile(
+ { state, commit, getters, dispatch },
+ { path: file.path, singleFile: true },
+ );
- // Wait for the fetchFileByFile dispatch to return, to trigger scrollToFile
- await waitForPromises();
+ // Wait for the fetchFileByFile dispatch to return, to trigger scrollToFile
+ await waitForPromises();
- expect(dispatch).toHaveBeenCalledWith('fetchFileByFile');
- expect(dispatch).toHaveBeenCalledWith('scrollToFile', file);
- expect(dispatch).toHaveBeenCalledTimes(2);
- });
+ expect(dispatch).toHaveBeenCalledWith('fetchFileByFile');
+ expect(dispatch).toHaveBeenCalledWith('scrollToFile', file);
+ expect(dispatch).toHaveBeenCalledTimes(2);
+ });
- it('shows an alert when there was an error fetching the file', async () => {
- dispatch = jest.fn().mockRejectedValue();
+ it('shows an alert when there was an error fetching the file', async () => {
+ dispatch = jest.fn().mockRejectedValue();
- diffActions.goToFile({ state, commit, getters, dispatch }, file);
+ diffActions.goToFile(
+ { state, commit, getters, dispatch },
+ { path: file.path, singleFile: true },
+ );
- // Wait for the fetchFileByFile dispatch to return, to trigger the catch
- await waitForPromises();
+ // Wait for the fetchFileByFile dispatch to return, to trigger the catch
+ await waitForPromises();
- expect(createAlert).toHaveBeenCalledTimes(1);
- expect(createAlert).toHaveBeenCalledWith({
- message: expect.stringMatching(LOAD_SINGLE_DIFF_FAILED),
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
+ message: expect.stringMatching(LOAD_SINGLE_DIFF_FAILED),
+ });
});
});
});
@@ -1690,12 +1707,22 @@ describe('DiffsStoreActions', () => {
it('commits SET_CURRENT_DIFF_FILE', () => {
return testAction(
diffActions.navigateToDiffFileIndex,
- 0,
+ { index: 0, singleFile: false },
{ flatBlobsList: [{ fileHash: '123' }] },
[{ type: types.SET_CURRENT_DIFF_FILE, payload: '123' }],
[],
);
});
+
+ it('dispatches the fetchFileByFile action when the state value viewDiffsFileByFile is true and the single-file file-by-file feature flag is enabled', () => {
+ return testAction(
+ diffActions.navigateToDiffFileIndex,
+ { index: 0, singleFile: true },
+ { viewDiffsFileByFile: true, flatBlobsList: [{ fileHash: '123' }] },
+ [{ type: types.SET_CURRENT_DIFF_FILE, payload: '123' }],
+ [{ type: 'fetchFileByFile' }],
+ );
+ });
});
describe('setFileByFile', () => {
diff --git a/spec/frontend/environment.js b/spec/frontend/environment.js
index 7b160c48501..4e341b2bb2f 100644
--- a/spec/frontend/environment.js
+++ b/spec/frontend/environment.js
@@ -21,8 +21,17 @@ class CustomEnvironment extends TestEnvironment {
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39496#note_503084332
setGlobalDateToFakeDate();
+ const { error: originalErrorFn } = context.console;
Object.assign(context.console, {
error(...args) {
+ if (
+ args?.[0]?.includes('[Vue warn]: Missing required prop') ||
+ args?.[0]?.includes('[Vue warn]: Invalid prop')
+ ) {
+ originalErrorFn.apply(context.console, args);
+ return;
+ }
+
throw new ErrorWithStack(
`Unexpected call of console.error() with:\n\n${args.join(', ')}`,
this.error,
@@ -30,7 +39,7 @@ class CustomEnvironment extends TestEnvironment {
},
warn(...args) {
- if (args[0].includes('The updateQuery callback for fetchMore is deprecated')) {
+ if (args?.[0]?.includes('The updateQuery callback for fetchMore is deprecated')) {
return;
}
throw new ErrorWithStack(
diff --git a/spec/frontend/monitoring/pages/dashboard_page_spec.js b/spec/frontend/monitoring/pages/dashboard_page_spec.js
index c5a8b50ee60..3de99673e71 100644
--- a/spec/frontend/monitoring/pages/dashboard_page_spec.js
+++ b/spec/frontend/monitoring/pages/dashboard_page_spec.js
@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import Dashboard from '~/monitoring/components/dashboard.vue';
import DashboardPage from '~/monitoring/pages/dashboard_page.vue';
import { createStore } from '~/monitoring/stores';
+import { assertProps } from 'helpers/assert_props';
import { dashboardProps } from '../fixture_data';
describe('monitoring/pages/dashboard_page', () => {
@@ -45,7 +46,7 @@ describe('monitoring/pages/dashboard_page', () => {
});
it('throws errors if dashboard props are not passed', () => {
- expect(() => buildWrapper()).toThrow('Missing required prop: "dashboardProps"');
+ expect(() => assertProps(DashboardPage, {})).toThrow('Missing required prop: "dashboardProps"');
});
it('renders the dashboard page with dashboard component', () => {
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js
index 2fea0a9199b..01089422376 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js
@@ -1,10 +1,10 @@
import { GlDropdownItem, GlIcon, GlDropdown } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import Vue, { nextTick } from 'vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { useFakeDate } from 'helpers/fake_date';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import waitForPromises from 'helpers/wait_for_promises';
import component from '~/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue';
@@ -22,37 +22,27 @@ import {
} from '~/packages_and_registries/container_registry/explorer/constants';
import getContainerRepositoryMetadata from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_metadata.query.graphql';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
-import { imageTagsCountMock } from '../../mock_data';
+import { containerRepositoryMock, imageTagsCountMock } from '../../mock_data';
describe('Details Header', () => {
let wrapper;
let apolloProvider;
const defaultImage = {
- name: 'foo',
- updatedAt: '2020-11-03T13:29:21Z',
- canDelete: true,
- project: {
- visibility: 'public',
- path: 'path',
- containerExpirationPolicy: {
- enabled: false,
- },
- },
+ ...containerRepositoryMock,
};
// set the date to Dec 4, 2020
useFakeDate(2020, 11, 4);
- const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
- const findLastUpdatedAndVisibility = () => findByTestId('updated-and-visibility');
- const findTitle = () => findByTestId('title');
- const findTagsCount = () => findByTestId('tags-count');
- const findCleanup = () => findByTestId('cleanup');
+ const findCreatedAndVisibility = () => wrapper.findByTestId('created-and-visibility');
+ const findTitle = () => wrapper.findByTestId('title');
+ const findTagsCount = () => wrapper.findByTestId('tags-count');
+ const findCleanup = () => wrapper.findByTestId('cleanup');
const findDeleteButton = () => wrapper.findComponent(GlDropdownItem);
const findInfoIcon = () => wrapper.findComponent(GlIcon);
const findMenu = () => wrapper.findComponent(GlDropdown);
- const findSize = () => findByTestId('image-size');
+ const findSize = () => wrapper.findByTestId('image-size');
const waitForMetadataItems = async () => {
// Metadata items are printed by a loop in the title-area and it takes two ticks for them to be available
@@ -69,7 +59,7 @@ describe('Details Header', () => {
const requestHandlers = [[getContainerRepositoryMetadata, resolver]];
apolloProvider = createMockApollo(requestHandlers);
- wrapper = shallowMount(component, {
+ wrapper = shallowMountExtended(component, {
apolloProvider,
propsData,
directives: {
@@ -97,7 +87,7 @@ describe('Details Header', () => {
});
it('root image shows project path name', () => {
- expect(findTitle().text()).toBe('path');
+ expect(findTitle().text()).toBe('gitlab-test');
});
it('has an icon', () => {
@@ -119,7 +109,7 @@ describe('Details Header', () => {
});
it('shows image.name', () => {
- expect(findTitle().text()).toContain('foo');
+ expect(findTitle().text()).toContain('rails-12009');
});
it('has no icon', () => {
@@ -287,12 +277,12 @@ describe('Details Header', () => {
);
});
- describe('visibility and updated at', () => {
- it('has last updated text', async () => {
+ describe('visibility and created at', () => {
+ it('has created text', async () => {
mountComponent();
await waitForMetadataItems();
- expect(findLastUpdatedAndVisibility().props('text')).toBe('Last updated 1 month ago');
+ expect(findCreatedAndVisibility().props('text')).toBe('Created Nov 3, 2020 13:29');
});
describe('visibility icon', () => {
@@ -300,7 +290,7 @@ describe('Details Header', () => {
mountComponent();
await waitForMetadataItems();
- expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye');
+ expect(findCreatedAndVisibility().props('icon')).toBe('eye');
});
it('shows an eye slashed when the project is not public', async () => {
mountComponent({
@@ -308,7 +298,7 @@ describe('Details Header', () => {
});
await waitForMetadataItems();
- expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye-slash');
+ expect(findCreatedAndVisibility().props('icon')).toBe('eye-slash');
});
});
});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
index cd54b856c97..8ca74f5077e 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
@@ -127,7 +127,6 @@ export const containerRepositoryMock = {
location: 'host.docker.internal:5000/gitlab-org/gitlab-test/rails-12009',
canDelete: true,
createdAt: '2020-11-03T13:29:21Z',
- updatedAt: '2020-11-03T13:29:21Z',
expirationPolicyStartedAt: null,
expirationPolicyCleanupStatus: 'UNSCHEDULED',
project: {
diff --git a/spec/frontend/releases/components/releases_sort_spec.js b/spec/frontend/releases/components/releases_sort_spec.js
index 92199896ab4..76907b4b8bb 100644
--- a/spec/frontend/releases/components/releases_sort_spec.js
+++ b/spec/frontend/releases/components/releases_sort_spec.js
@@ -1,5 +1,6 @@
import { GlSorting, GlSortingItem } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { assertProps } from 'helpers/assert_props';
import ReleasesSort from '~/releases/components/releases_sort.vue';
import { RELEASED_AT_ASC, RELEASED_AT_DESC, CREATED_ASC, CREATED_DESC } from '~/releases/constants';
@@ -92,7 +93,7 @@ describe('releases_sort.vue', () => {
describe('prop validation', () => {
it('validates that the `value` prop is one of the expected sort strings', () => {
expect(() => {
- createComponent('not a valid value');
+ assertProps(ReleasesSort, { value: 'not a valid value' });
}).toThrow('Invalid prop: custom validator check failed');
});
});
diff --git a/spec/frontend/super_sidebar/components/groups_list_spec.js b/spec/frontend/super_sidebar/components/groups_list_spec.js
index 66397153d71..4b61991a5db 100644
--- a/spec/frontend/super_sidebar/components/groups_list_spec.js
+++ b/spec/frontend/super_sidebar/components/groups_list_spec.js
@@ -75,7 +75,7 @@ describe('GroupsList component', () => {
it('passes the correct props to the frequent items list', () => {
expect(findFrequentItemsList().props()).toEqual({
- title: s__('Navigation|Frequent groups'),
+ title: s__('Navigation|Frequently visited groups'),
storageKey,
maxItems: MAX_FREQUENT_GROUPS_COUNT,
pristineText: s__('Navigation|Groups you visit often will appear here.'),
diff --git a/spec/frontend/super_sidebar/components/projects_list_spec.js b/spec/frontend/super_sidebar/components/projects_list_spec.js
index 3500e0acd6c..284b88ef27e 100644
--- a/spec/frontend/super_sidebar/components/projects_list_spec.js
+++ b/spec/frontend/super_sidebar/components/projects_list_spec.js
@@ -70,7 +70,7 @@ describe('ProjectsList component', () => {
it('passes the correct props to the frequent items list', () => {
expect(findFrequentItemsList().props()).toEqual({
- title: s__('Navigation|Frequent projects'),
+ title: s__('Navigation|Frequently visited projects'),
storageKey,
maxItems: MAX_FREQUENT_PROJECTS_COUNT,
pristineText: s__('Navigation|Projects you visit often will appear here.'),
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
index ea7dabf34c9..5be0f1a64a9 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
@@ -3,6 +3,7 @@ import * as Sentry from '@sentry/browser';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import waitForPromises from 'helpers/wait_for_promises';
+import { assertProps } from 'helpers/assert_props';
import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
import ActionButtons from '~/vue_merge_request_widget/components/widget/action_buttons.vue';
import Widget from '~/vue_merge_request_widget/components/widget/widget.vue';
@@ -111,9 +112,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
it('validates widget name', () => {
expect(() => {
- createComponent({
- propsData: { widgetName: 'InvalidWidgetName' },
- });
+ assertProps(Widget, { widgetName: 'InvalidWidgetName' });
}).toThrow();
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
index 3964d895b4b..ff4c66e534b 100644
--- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
@@ -9,6 +9,7 @@ import ContentEditor from '~/content_editor/components/content_editor.vue';
import BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import { assertProps } from 'helpers/assert_props';
import { stubComponent } from 'helpers/stub_component';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -33,23 +34,26 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
const autocompleteDataSources = { commands: '/foobar/-/autcomplete_sources' };
let mock;
+ const defaultProps = {
+ value,
+ renderMarkdownPath,
+ markdownDocsPath,
+ quickActionsDocsPath,
+ enableAutocomplete,
+ autocompleteDataSources,
+ enablePreview,
+ formFieldProps: {
+ id: formFieldId,
+ name: formFieldName,
+ placeholder: formFieldPlaceholder,
+ 'aria-label': formFieldAriaLabel,
+ },
+ };
const buildWrapper = ({ propsData = {}, attachTo, stubs = {} } = {}) => {
wrapper = mountExtended(MarkdownEditor, {
attachTo,
propsData: {
- value,
- renderMarkdownPath,
- markdownDocsPath,
- quickActionsDocsPath,
- enableAutocomplete,
- autocompleteDataSources,
- enablePreview,
- formFieldProps: {
- id: formFieldId,
- name: formFieldName,
- placeholder: formFieldPlaceholder,
- 'aria-label': formFieldAriaLabel,
- },
+ ...defaultProps,
...propsData,
},
stubs: {
@@ -244,9 +248,9 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
});
it('fails to render if textarea id and name is not passed', () => {
- expect(() => {
- buildWrapper({ propsData: { formFieldProps: {} } });
- }).toThrow('Invalid prop: custom validator check failed for prop "formFieldProps"');
+ expect(() => assertProps(MarkdownEditor, { ...defaultProps, formFieldProps: {} })).toThrow(
+ 'Invalid prop: custom validator check failed for prop "formFieldProps"',
+ );
});
it(`emits ${EDITING_MODE_CONTENT_EDITOR} event when enableContentEditor emitted from markdown editor`, async () => {
diff --git a/spec/frontend/vue_shared/components/slot_switch_spec.js b/spec/frontend/vue_shared/components/slot_switch_spec.js
index f25b9877aba..daca4977817 100644
--- a/spec/frontend/vue_shared/components/slot_switch_spec.js
+++ b/spec/frontend/vue_shared/components/slot_switch_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { assertProps } from 'helpers/assert_props';
import SlotSwitch from '~/vue_shared/components/slot_switch.vue';
@@ -26,7 +27,9 @@ describe('SlotSwitch', () => {
});
it('throws an error if activeSlotNames is missing', () => {
- expect(createComponent).toThrow('[Vue warn]: Missing required prop: "activeSlotNames"');
+ expect(() => assertProps(SlotSwitch, {})).toThrow(
+ '[Vue warn]: Missing required prop: "activeSlotNames"',
+ );
});
it('renders no slots if activeSlotNames is empty', () => {
diff --git a/spec/frontend/vue_shared/components/split_button_spec.js b/spec/frontend/vue_shared/components/split_button_spec.js
index 6b869db4058..ffa25ae8448 100644
--- a/spec/frontend/vue_shared/components/split_button_spec.js
+++ b/spec/frontend/vue_shared/components/split_button_spec.js
@@ -2,6 +2,7 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { assertProps } from 'helpers/assert_props';
import SplitButton from '~/vue_shared/components/split_button.vue';
const mockActionItems = [
@@ -42,12 +43,12 @@ describe('SplitButton', () => {
it('fails for empty actionItems', () => {
const actionItems = [];
- expect(() => createComponent({ actionItems })).toThrow();
+ expect(() => assertProps(SplitButton, { actionItems })).toThrow();
});
it('fails for single actionItems', () => {
const actionItems = [mockActionItems[0]];
- expect(() => createComponent({ actionItems })).toThrow();
+ expect(() => assertProps(SplitButton, { actionItems })).toThrow();
});
it('renders actionItems', () => {
diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js
index a4bf2386ed4..dee781209ac 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -388,11 +388,12 @@ describe('WorkItemDetail component', () => {
expect(findParent().exists()).toBe(false);
});
- it('shows work item type if there is not a parent', async () => {
+ it('shows work item type with reference when there is no a parent', async () => {
createComponent({ handler: jest.fn().mockResolvedValue(workItemQueryResponseWithoutParent) });
await waitForPromises();
expect(findWorkItemType().exists()).toBe(true);
+ expect(findWorkItemType().text()).toBe('Task #1');
});
describe('with parent', () => {
diff --git a/spec/lib/gitlab/git/blame_mode_spec.rb b/spec/lib/gitlab/git/blame_mode_spec.rb
new file mode 100644
index 00000000000..1fc6f12c552
--- /dev/null
+++ b/spec/lib/gitlab/git/blame_mode_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Git::BlameMode, feature_category: :source_code_management do
+ subject(:blame_mode) { described_class.new(project, params) }
+
+ let_it_be(:project) { build(:project) }
+ let(:params) { {} }
+
+ describe '#streaming_supported?' do
+ subject { blame_mode.streaming_supported? }
+
+ it { is_expected.to be_truthy }
+
+ context 'when `blame_page_streaming` is disabled' do
+ before do
+ stub_feature_flags(blame_page_streaming: false)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#streaming?' do
+ subject { blame_mode.streaming? }
+
+ it { is_expected.to be_falsey }
+
+ context 'when streaming param is provided' do
+ let(:params) { { streaming: true } }
+
+ it { is_expected.to be_truthy }
+
+ context 'when `blame_page_streaming` is disabled' do
+ before do
+ stub_feature_flags(blame_page_streaming: false)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ describe '#pagination?' do
+ subject { blame_mode.pagination? }
+
+ it { is_expected.to be_truthy }
+
+ context 'when `streaming` params is enabled' do
+ let(:params) { { streaming: true } }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when `no_pagination` param is provided' do
+ let(:params) { { no_pagination: true } }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when `blame_page_pagination` is disabled' do
+ before do
+ stub_feature_flags(blame_page_pagination: false)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#full?' do
+ subject { blame_mode.full? }
+
+ it { is_expected.to be_falsey }
+
+ context 'when `blame_page_pagination` is disabled' do
+ before do
+ stub_feature_flags(blame_page_pagination: false)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+end
diff --git a/spec/services/projects/blame_service_spec.rb b/spec/services/projects/blame_service_spec.rb
index e3df69b3b7b..7f7f48cf622 100644
--- a/spec/services/projects/blame_service_spec.rb
+++ b/spec/services/projects/blame_service_spec.rb
@@ -3,12 +3,13 @@
require 'spec_helper'
RSpec.describe Projects::BlameService, :aggregate_failures, feature_category: :source_code_management do
- subject(:service) { described_class.new(blob, commit, params) }
+ subject(:service) { described_class.new(blob, commit, blame_mode, params) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:commit) { project.repository.commit }
let_it_be(:blob) { project.repository.blob_at('HEAD', 'README.md') }
+ let(:blame_mode) { Gitlab::Git::BlameMode.new(project, params) }
let(:params) { { page: page } }
let(:page) { nil }
diff --git a/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb
index 1d79a61fbb0..17e48d6b581 100644
--- a/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb
@@ -23,6 +23,7 @@ RSpec.shared_examples 'accept package tags request' do |status:|
end
it_behaves_like 'returning response status', status
+ it_behaves_like 'track event', :list_tags
it 'returns a valid json response' do
subject
@@ -63,6 +64,7 @@ RSpec.shared_examples 'accept create package tag request' do |user_type|
end
it_behaves_like 'returning response status', :no_content
+ it_behaves_like 'track event', :create_tag
it 'creates the package tag' do
expect { subject }.to change { Packages::Tag.count }.by(1)
@@ -145,6 +147,7 @@ RSpec.shared_examples 'accept delete package tag request' do |user_type|
end
it_behaves_like 'returning response status', :no_content
+ it_behaves_like 'track event', :delete_tag
it 'returns a valid response' do
subject
@@ -190,3 +193,21 @@ RSpec.shared_examples 'accept delete package tag request' do |user_type|
end
end
end
+
+RSpec.shared_examples 'track event' do |event_name|
+ let(:event_user) do
+ if auth == :deploy_token
+ deploy_token
+ elsif user_role
+ user
+ end
+ end
+
+ let(:snowplow_gitlab_standard_context) do
+ { project: project, namespace: project.namespace, property: 'i_package_npm_user' }.tap do |context|
+ context[:user] = event_user if event_user
+ end
+ end
+
+ it_behaves_like 'a package tracking event', described_class.name, event_name.to_s
+end