Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
7083ff8c14
commit
08d259cc2b
2
Gemfile
2
Gemfile
|
|
@ -588,7 +588,7 @@ group :test do
|
|||
|
||||
gem 'graphlyte', '~> 1.0.0', feature_category: :shared
|
||||
|
||||
gem 'shoulda-matchers', '~> 5.1.0', require: false, feature_category: :shared
|
||||
gem 'shoulda-matchers', '~> 6.4.0', require: false, feature_category: :shared
|
||||
gem 'email_spec', '~> 2.3.0', feature_category: :shared
|
||||
gem 'webmock', '~> 3.25.0', feature_category: :shared
|
||||
gem 'rails-controller-testing', feature_category: :shared
|
||||
|
|
|
|||
|
|
@ -685,7 +685,7 @@
|
|||
{"name":"sentry-ruby","version":"5.22.1","platform":"ruby","checksum":"ed77bdd76da7a4c6a3de43dc6d19d3c0412b2675b014a2654bc5bafd4d5b3289"},
|
||||
{"name":"sentry-sidekiq","version":"5.22.1","platform":"ruby","checksum":"bd7a3f915e58e13ea67251d9a458667fc4bee6dfbbd12614c47daa239e822a89"},
|
||||
{"name":"shellany","version":"0.0.1","platform":"ruby","checksum":"0e127a9132698766d7e752e82cdac8250b6adbd09e6c0a7fbbb6f61964fedee7"},
|
||||
{"name":"shoulda-matchers","version":"5.1.0","platform":"ruby","checksum":"a01d20589989e9653ab4a28c67d9db2b82bcf0a2496cf01d5e1a95a4aaaf5b07"},
|
||||
{"name":"shoulda-matchers","version":"6.4.0","platform":"ruby","checksum":"9055bb7f4bb342125fb860809798855c630e05ef5e75837b3168b8e6ee1608b0"},
|
||||
{"name":"sidekiq-cron","version":"1.12.0","platform":"ruby","checksum":"6663080a454088bd88773a0da3ae91e554b8a2e8b06cfc629529a83fd1a3096c"},
|
||||
{"name":"sigdump","version":"0.2.5","platform":"ruby","checksum":"bb706c1cce70458b285d2c3a57121e801ccb79f68be7f7377692eb40b5437242"},
|
||||
{"name":"signet","version":"0.18.0","platform":"ruby","checksum":"66cda8c2edc2dde25090b792e7e6fc9598c3c2bdd64ffacd89f1ffe3cb9cea3b"},
|
||||
|
|
|
|||
|
|
@ -1751,7 +1751,7 @@ GEM
|
|||
sentry-ruby (~> 5.22.1)
|
||||
sidekiq (>= 3.0)
|
||||
shellany (0.0.1)
|
||||
shoulda-matchers (5.1.0)
|
||||
shoulda-matchers (6.4.0)
|
||||
activesupport (>= 5.2.0)
|
||||
sidekiq-cron (1.12.0)
|
||||
fugit (~> 1.8)
|
||||
|
|
@ -2312,7 +2312,7 @@ DEPENDENCIES
|
|||
sentry-rails (~> 5.22.0)
|
||||
sentry-ruby (~> 5.22.0)
|
||||
sentry-sidekiq (~> 5.22.0)
|
||||
shoulda-matchers (~> 5.1.0)
|
||||
shoulda-matchers (~> 6.4.0)
|
||||
sidekiq!
|
||||
sidekiq-cron (~> 1.12.0)
|
||||
sigdump (~> 0.2.4)
|
||||
|
|
|
|||
|
|
@ -696,7 +696,7 @@
|
|||
{"name":"sentry-ruby","version":"5.22.1","platform":"ruby","checksum":"ed77bdd76da7a4c6a3de43dc6d19d3c0412b2675b014a2654bc5bafd4d5b3289"},
|
||||
{"name":"sentry-sidekiq","version":"5.22.1","platform":"ruby","checksum":"bd7a3f915e58e13ea67251d9a458667fc4bee6dfbbd12614c47daa239e822a89"},
|
||||
{"name":"shellany","version":"0.0.1","platform":"ruby","checksum":"0e127a9132698766d7e752e82cdac8250b6adbd09e6c0a7fbbb6f61964fedee7"},
|
||||
{"name":"shoulda-matchers","version":"5.1.0","platform":"ruby","checksum":"a01d20589989e9653ab4a28c67d9db2b82bcf0a2496cf01d5e1a95a4aaaf5b07"},
|
||||
{"name":"shoulda-matchers","version":"6.4.0","platform":"ruby","checksum":"9055bb7f4bb342125fb860809798855c630e05ef5e75837b3168b8e6ee1608b0"},
|
||||
{"name":"sidekiq-cron","version":"1.12.0","platform":"ruby","checksum":"6663080a454088bd88773a0da3ae91e554b8a2e8b06cfc629529a83fd1a3096c"},
|
||||
{"name":"sigdump","version":"0.2.5","platform":"ruby","checksum":"bb706c1cce70458b285d2c3a57121e801ccb79f68be7f7377692eb40b5437242"},
|
||||
{"name":"signet","version":"0.18.0","platform":"ruby","checksum":"66cda8c2edc2dde25090b792e7e6fc9598c3c2bdd64ffacd89f1ffe3cb9cea3b"},
|
||||
|
|
|
|||
|
|
@ -1784,7 +1784,7 @@ GEM
|
|||
sentry-ruby (~> 5.22.1)
|
||||
sidekiq (>= 3.0)
|
||||
shellany (0.0.1)
|
||||
shoulda-matchers (5.1.0)
|
||||
shoulda-matchers (6.4.0)
|
||||
activesupport (>= 5.2.0)
|
||||
sidekiq-cron (1.12.0)
|
||||
fugit (~> 1.8)
|
||||
|
|
@ -2346,7 +2346,7 @@ DEPENDENCIES
|
|||
sentry-rails (~> 5.22.0)
|
||||
sentry-ruby (~> 5.22.0)
|
||||
sentry-sidekiq (~> 5.22.0)
|
||||
shoulda-matchers (~> 5.1.0)
|
||||
shoulda-matchers (~> 6.4.0)
|
||||
sidekiq!
|
||||
sidekiq-cron (~> 1.12.0)
|
||||
sigdump (~> 0.2.4)
|
||||
|
|
|
|||
|
|
@ -365,6 +365,7 @@ export default {
|
|||
:class="{
|
||||
'right-sidebar-expanded': isSidebarOpen,
|
||||
'right-sidebar-collapsed': !isSidebarOpen,
|
||||
'!gl-bottom-8': displayStickyFooter,
|
||||
}"
|
||||
:artifact-help-url="artifactHelpUrl"
|
||||
data-testid="job-sidebar"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import { GlToast } from '@gitlab/ui';
|
||||
import Vue from 'vue';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import Sidebar from '~/right_sidebar';
|
||||
import { getSidebarOptions } from '~/sidebar/mount_sidebar';
|
||||
import CsvImportExportButtons from './components/csv_import_export_buttons.vue';
|
||||
import IssuableByEmail from './components/issuable_by_email.vue';
|
||||
import issuableBulkUpdateActions from './issuable_bulk_update_actions';
|
||||
import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
|
||||
import IssuableContext from './issuable_context';
|
||||
|
|
@ -63,39 +61,6 @@ export function initCsvImportExportButtons() {
|
|||
});
|
||||
}
|
||||
|
||||
export function initIssuableByEmail() {
|
||||
const el = document.querySelector('.js-issuable-by-email');
|
||||
|
||||
if (!el) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Vue.use(GlToast);
|
||||
|
||||
const {
|
||||
initialEmail,
|
||||
issuableType,
|
||||
emailsHelpPagePath,
|
||||
quickActionsHelpPath,
|
||||
markdownHelpPath,
|
||||
resetPath,
|
||||
} = el.dataset;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
name: 'IssuableByEmailRoot',
|
||||
provide: {
|
||||
initialEmail,
|
||||
issuableType,
|
||||
emailsHelpPagePath,
|
||||
quickActionsHelpPath,
|
||||
markdownHelpPath,
|
||||
resetPath,
|
||||
},
|
||||
render: (createElement) => createElement(IssuableByEmail),
|
||||
});
|
||||
}
|
||||
|
||||
export function initIssuableSidebar() {
|
||||
const el = document.querySelector('.js-sidebar-options');
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import VueRouter from 'vue-router';
|
|||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import { defaultClient } from '~/graphql_shared/issuable_client';
|
||||
import MergeRequestsListApp from './components/merge_requests_list_app.vue';
|
||||
import MoreactionsDropdown from './components/more_actions_dropdown.vue';
|
||||
|
||||
export async function mountMergeRequestListsApp({
|
||||
getMergeRequestsQuery,
|
||||
|
|
@ -94,36 +93,3 @@ export async function mountMergeRequestListsApp({
|
|||
render: (createComponent) => createComponent(MergeRequestsListApp),
|
||||
});
|
||||
}
|
||||
|
||||
export async function mountMoreActionsDropdown() {
|
||||
const el = document.querySelector('#js-vue-mr-list-more-actions');
|
||||
|
||||
if (!el) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
isSignedIn,
|
||||
showExportButton,
|
||||
issuableType,
|
||||
issuableCount,
|
||||
email,
|
||||
exportCsvPath,
|
||||
rssUrl,
|
||||
} = el.dataset;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
name: 'MergeRequestsListMoreActions',
|
||||
provide: {
|
||||
isSignedIn: parseBoolean(isSignedIn),
|
||||
showExportButton: parseBoolean(showExportButton),
|
||||
issuableType,
|
||||
issuableCount: Number(issuableCount),
|
||||
email,
|
||||
exportCsvPath,
|
||||
rssUrl,
|
||||
},
|
||||
render: (createComponent) => createComponent(MoreactionsDropdown),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,11 @@ import { debounce, isEqual } from 'lodash';
|
|||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import { s__, __, sprintf } from '~/locale';
|
||||
import createStore from '../stores';
|
||||
|
||||
const SEARCH_DEBOUNCE_MS = 250;
|
||||
|
||||
export default {
|
||||
name: 'MilestoneCombobox',
|
||||
store: createStore(),
|
||||
components: {
|
||||
GlCollapsibleListbox,
|
||||
GlBadge,
|
||||
|
|
@ -50,8 +48,8 @@ export default {
|
|||
unselect: __('Unselect'),
|
||||
},
|
||||
computed: {
|
||||
...mapState(['matches', 'selectedMilestones']),
|
||||
...mapGetters(['isLoading']),
|
||||
...mapState('milestoneCombobox', ['matches', 'selectedMilestones']),
|
||||
...mapGetters('milestoneCombobox', ['isLoading']),
|
||||
allMilestones() {
|
||||
const { groupMilestones, projectMilestones } = this.matches || {};
|
||||
const milestones = [];
|
||||
|
|
@ -125,7 +123,7 @@ export default {
|
|||
this.fetchMilestones();
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
...mapActions('milestoneCombobox', [
|
||||
'setProjectId',
|
||||
'setGroupId',
|
||||
'setGroupMilestonesAvailable',
|
||||
|
|
|
|||
|
|
@ -8,10 +8,19 @@ import createState from './state';
|
|||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export const createMilestoneComboboxModule = () => ({
|
||||
actions,
|
||||
getters,
|
||||
mutations,
|
||||
state: createState(),
|
||||
});
|
||||
|
||||
export default () =>
|
||||
new Vuex.Store({
|
||||
actions,
|
||||
getters,
|
||||
mutations,
|
||||
state: createState(),
|
||||
modules: {
|
||||
milestoneCombobox: {
|
||||
namespaced: true,
|
||||
...createMilestoneComboboxModule(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -40,12 +40,16 @@ export default {
|
|||
};
|
||||
},
|
||||
modelsCountLabel() {
|
||||
return n__('MlModelRegistry|%d experiment', 'MlModelRegistry|%d experiments', this.count);
|
||||
return n__(
|
||||
'MlExperimentTracking|%d experiment',
|
||||
'MlExperimentTracking|%d experiments',
|
||||
this.count,
|
||||
);
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
createTitle: s__('MlModelRegistry|Create'),
|
||||
importMlflow: s__('MlModelRegistry|Create experiments using MLflow'),
|
||||
createTitle: s__('MlExperimentTracking|Create'),
|
||||
importMlflow: s__('MlExperimentTracking|Create experiments using MLflow'),
|
||||
},
|
||||
mlflowModalId: MLFLOW_USAGE_MODAL_ID,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -77,8 +77,8 @@ export default {
|
|||
}, DEFAULT_DEBOUNCE_AND_THROTTLE_MS),
|
||||
},
|
||||
i18n: {
|
||||
noResultsMessage: s__('MlModelRegistry|No results'),
|
||||
emptyFieldPlaceholder: s__('MlModelRegistry|Select a model'),
|
||||
noResultsMessage: s__('MlExperimentTracking|No results'),
|
||||
emptyFieldPlaceholder: s__('MlExperimentTracking|Select a model'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -43,13 +43,15 @@ export default {
|
|||
versionDescription() {
|
||||
if (this.latestVersion) {
|
||||
return sprintf(
|
||||
s__('MlModelRegistry|Must be a semantic version. Latest version is %{latestVersion}'),
|
||||
s__(
|
||||
'MlExperimentTracking|Must be a semantic version. Latest version is %{latestVersion}',
|
||||
),
|
||||
{
|
||||
latestVersion: this.latestVersion,
|
||||
},
|
||||
);
|
||||
}
|
||||
return s__('MlModelRegistry|Must be a semantic version.');
|
||||
return s__('MlExperimentTracking|Must be a semantic version.');
|
||||
},
|
||||
autocompleteDataSources() {
|
||||
return gl.GfmAutoComplete?.dataSources;
|
||||
|
|
@ -123,26 +125,28 @@ export default {
|
|||
},
|
||||
},
|
||||
descriptionFormFieldProps: {
|
||||
placeholder: s__('MlModelRegistry|Enter a model version description'),
|
||||
placeholder: s__('MlExperimentTracking|Enter a model version description'),
|
||||
id: 'model-version-description',
|
||||
name: 'model-version-description',
|
||||
},
|
||||
i18n: {
|
||||
actionPrimaryText: s__('MlModelRegistry|Promote'),
|
||||
actionPrimaryText: s__('MlExperimentTracking|Promote'),
|
||||
actionSecondaryText: __('Cancel'),
|
||||
versionDescription: s__('MlModelRegistry|Enter a semantic version.'),
|
||||
versionValid: s__('MlModelRegistry|Version is valid semantic version.'),
|
||||
versionInvalid: s__('MlModelRegistry|Version is not a valid semantic version.'),
|
||||
versionPlaceholder: s__('MlModelRegistry|For example 1.0.0'),
|
||||
descriptionPlaceholder: s__('MlModelRegistry|Enter some description'),
|
||||
title: s__('MlModelRegistry|Promote run'),
|
||||
description: s__('MlModelRegistry|Complete the form below to promote run to a model version.'),
|
||||
optionalText: s__('MlModelRegistry|(Optional)'),
|
||||
versionLabelText: s__('MlModelRegistry|Version'),
|
||||
versionDescriptionText: s__('MlModelRegistry|Description'),
|
||||
modelSelectionLabelText: s__('MlModelRegistry|Model'),
|
||||
versionDescription: s__('MlExperimentTracking|Enter a semantic version.'),
|
||||
versionValid: s__('MlExperimentTracking|Version is valid semantic version.'),
|
||||
versionInvalid: s__('MlExperimentTracking|Version is not a valid semantic version.'),
|
||||
versionPlaceholder: s__('MlExperimentTracking|For example 1.0.0'),
|
||||
descriptionPlaceholder: s__('MlExperimentTracking|Enter some description'),
|
||||
title: s__('MlExperimentTracking|Promote run'),
|
||||
description: s__(
|
||||
'MlExperimentTracking|Complete the form below to promote run to a model version.',
|
||||
),
|
||||
optionalText: s__('MlExperimentTracking|(Optional)'),
|
||||
versionLabelText: s__('MlExperimentTracking|Version'),
|
||||
versionDescriptionText: s__('MlExperimentTracking|Description'),
|
||||
modelSelectionLabelText: s__('MlExperimentTracking|Model'),
|
||||
modelDescription: s__(
|
||||
'MlModelRegistry|Select the model that will contain the new version. The run will move to the default experiment of that model.',
|
||||
'MlExperimentTracking|Select the model that will contain the new version. The run will move to the default experiment of that model.',
|
||||
),
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -55,11 +55,11 @@ export default {
|
|||
|
||||
const fields = range(maxStep + 1).map((step) => ({
|
||||
key: step.toString(),
|
||||
label: sprintf(s__('MlModelRegistry|Step %{step}'), { step }),
|
||||
label: sprintf(s__('MlExperimentTracking|Step %{step}'), { step }),
|
||||
...cssClasses,
|
||||
}));
|
||||
|
||||
return [{ key: 'name', label: s__('MlModelRegistry|Metric'), ...cssClasses }, ...fields];
|
||||
return [{ key: 'name', label: s__('MlExperimentTracking|Metric'), ...cssClasses }, ...fields];
|
||||
},
|
||||
metricsTableItems() {
|
||||
const items = {};
|
||||
|
|
@ -87,20 +87,20 @@ export default {
|
|||
},
|
||||
},
|
||||
i18n: {
|
||||
detailsLabel: s__('MlModelRegistry|Details & Metadata'),
|
||||
artifactsLabel: s__('MlModelRegistry|Artifacts'),
|
||||
mlflowIdLabel: s__('MlModelRegistry|MLflow run ID'),
|
||||
ciSectionLabel: s__('MlModelRegistry|CI Info'),
|
||||
detailsLabel: s__('MlExperimentTracking|Details & Metadata'),
|
||||
artifactsLabel: s__('MlExperimentTracking|Artifacts'),
|
||||
mlflowIdLabel: s__('MlExperimentTracking|MLflow run ID'),
|
||||
ciSectionLabel: s__('MlExperimentTracking|CI Info'),
|
||||
jobLabel: __('Job'),
|
||||
ciUserLabel: s__('MlModelRegistry|Triggered by'),
|
||||
ciUserLabel: s__('MlExperimentTracking|Triggered by'),
|
||||
ciMrLabel: __('Merge request'),
|
||||
parametersLabel: s__('MlModelRegistry|Parameters'),
|
||||
performanceLabel: s__('MlModelRegistry|Performance'),
|
||||
noParametersMessage: s__('MlModelRegistry|No logged parameters'),
|
||||
noMetricsMessage: s__('MlModelRegistry|No logged metrics'),
|
||||
noMetadataMessage: s__('MlModelRegistry|No logged metadata'),
|
||||
noCiMessage: s__('MlModelRegistry|Run not linked to a CI build'),
|
||||
noArtifactsMessage: s__('MlModelRegistry|No logged artifacts.'),
|
||||
parametersLabel: s__('MlExperimentTracking|Parameters'),
|
||||
performanceLabel: s__('MlExperimentTracking|Performance'),
|
||||
noParametersMessage: s__('MlExperimentTracking|No logged parameters'),
|
||||
noMetricsMessage: s__('MlExperimentTracking|No logged metrics'),
|
||||
noMetadataMessage: s__('MlExperimentTracking|No logged metadata'),
|
||||
noCiMessage: s__('MlExperimentTracking|Run not linked to a CI build'),
|
||||
noArtifactsMessage: s__('MlExperimentTracking|No logged artifacts.'),
|
||||
copyMessage: __('Copy MLflow run ID'),
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ export default {
|
|||
},
|
||||
error(error) {
|
||||
this.errorMessage = sprintf(
|
||||
s__('MlModelRegistry|Failed to load experiments with error: %{error}'),
|
||||
s__('MlExperimentTracking|Failed to load experiments with error: %{error}'),
|
||||
{ error: error.message },
|
||||
);
|
||||
Sentry.captureException(error);
|
||||
|
|
|
|||
|
|
@ -1,32 +1,13 @@
|
|||
import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
|
||||
import getMergeRequestsQuery from 'ee_else_ce/merge_requests/list/queries/group/get_merge_requests.query.graphql';
|
||||
import getMergeRequestsCountsQuery from 'ee_else_ce/merge_requests/list/queries/group/get_merge_requests_counts.query.graphql';
|
||||
import getMergeRequestsApprovalsQuery from 'ee_else_ce/merge_requests/list/queries/group/get_merge_requests_approvals.query.graphql';
|
||||
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
|
||||
import { FILTERED_SEARCH } from '~/filtered_search/constants';
|
||||
import { initBulkUpdateSidebar } from '~/issuable';
|
||||
import initFilteredSearch from '~/pages/search/init_filtered_search';
|
||||
import { initNewResourceDropdown } from '~/vue_shared/components/new_resource_dropdown/init_new_resource_dropdown';
|
||||
import { RESOURCE_TYPE_MERGE_REQUEST } from '~/vue_shared/components/new_resource_dropdown/constants';
|
||||
import searchUserGroupProjectsWithMergeRequestsEnabled from '~/vue_shared/components/new_resource_dropdown/graphql/search_user_group_projects_with_merge_requests_enabled.query.graphql';
|
||||
import { mountMergeRequestListsApp } from '~/merge_requests/list';
|
||||
|
||||
const ISSUABLE_BULK_UPDATE_PREFIX = 'merge_request_';
|
||||
|
||||
addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys);
|
||||
initBulkUpdateSidebar(ISSUABLE_BULK_UPDATE_PREFIX);
|
||||
|
||||
initFilteredSearch({
|
||||
page: FILTERED_SEARCH.MERGE_REQUESTS,
|
||||
isGroupDecendent: true,
|
||||
useDefaultState: true,
|
||||
filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
|
||||
});
|
||||
initNewResourceDropdown({
|
||||
resourceType: RESOURCE_TYPE_MERGE_REQUEST,
|
||||
query: searchUserGroupProjectsWithMergeRequestsEnabled,
|
||||
extractProjects: (data) => data?.group?.projects?.nodes,
|
||||
});
|
||||
mountMergeRequestListsApp({
|
||||
getMergeRequestsQuery,
|
||||
getMergeRequestsCountsQuery,
|
||||
|
|
|
|||
|
|
@ -1,31 +1,15 @@
|
|||
import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
|
||||
import getMergeRequestsQuery from 'ee_else_ce/merge_requests/list/queries/project/get_merge_requests.query.graphql';
|
||||
import getMergeRequestsCountsQuery from 'ee_else_ce/merge_requests/list/queries/project/get_merge_requests_counts.query.graphql';
|
||||
import getMergeRequestsApprovalsQuery from 'ee_else_ce/merge_requests/list/queries/project/get_merge_requests_approvals.query.graphql';
|
||||
import { addShortcutsExtension } from '~/behaviors/shortcuts';
|
||||
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
|
||||
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
|
||||
import { FILTERED_SEARCH } from '~/filtered_search/constants';
|
||||
import { initBulkUpdateSidebar, initCsvImportExportButtons, initIssuableByEmail } from '~/issuable';
|
||||
import initFilteredSearch from '~/pages/search/init_filtered_search';
|
||||
import { mountMoreActionsDropdown, mountMergeRequestListsApp } from '~/merge_requests/list';
|
||||
import { initBulkUpdateSidebar } from '~/issuable';
|
||||
import { mountMergeRequestListsApp } from '~/merge_requests/list';
|
||||
|
||||
initBulkUpdateSidebar('merge_request_');
|
||||
|
||||
addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys);
|
||||
IssuableFilteredSearchTokenKeys.removeTokensForKeys('iteration');
|
||||
|
||||
initFilteredSearch({
|
||||
page: FILTERED_SEARCH.MERGE_REQUESTS,
|
||||
filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
|
||||
useDefaultState: true,
|
||||
});
|
||||
|
||||
addShortcutsExtension(ShortcutsNavigation);
|
||||
|
||||
initIssuableByEmail();
|
||||
initCsvImportExportButtons();
|
||||
mountMoreActionsDropdown();
|
||||
mountMergeRequestListsApp({
|
||||
getMergeRequestsQuery,
|
||||
getMergeRequestsApprovalsQuery,
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export default {
|
|||
const bars = [
|
||||
{ name: s__('Pipeline|Successful'), data: [] },
|
||||
{ name: s__('Pipeline|Failed'), data: [] },
|
||||
{ name: s__('Pipeline|Other'), data: [] },
|
||||
{ name: s__('Pipeline|Other (Cancelled, Skipped)'), data: [] },
|
||||
];
|
||||
|
||||
this.timeSeries.forEach(({ label, successCount, failedCount, otherCount }) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import Vue from 'vue';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import Vuex from 'vuex';
|
||||
import { createMilestoneComboboxModule } from '~/milestones/stores';
|
||||
import ReleaseEditNewApp from './components/app_edit_new.vue';
|
||||
import createStore from './stores';
|
||||
import createEditNewModule from './stores/modules/edit_new';
|
||||
|
|
@ -12,6 +13,7 @@ export default () => {
|
|||
|
||||
const store = createStore({
|
||||
modules: {
|
||||
milestoneCombobox: { namespaced: true, ...createMilestoneComboboxModule() },
|
||||
editNew: createEditNewModule({ ...el.dataset, isExistingRelease: true }),
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import Vue from 'vue';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import Vuex from 'vuex';
|
||||
import { createMilestoneComboboxModule } from '~/milestones/stores';
|
||||
import { createRefModule } from '../ref/stores';
|
||||
import ReleaseEditNewApp from './components/app_edit_new.vue';
|
||||
import createStore from './stores';
|
||||
|
|
@ -13,6 +14,7 @@ export default () => {
|
|||
|
||||
const store = createStore({
|
||||
modules: {
|
||||
milestoneCombobox: { namespaced: true, ...createMilestoneComboboxModule() },
|
||||
editNew: createEditNewModule({ ...el.dataset, isExistingRelease: false }),
|
||||
ref: createRefModule(),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -149,11 +149,7 @@ class GroupsController < Groups::ApplicationController
|
|||
@badge_api_endpoint = expose_path(api_v4_groups_badges_path(id: @group.id))
|
||||
end
|
||||
|
||||
def merge_requests
|
||||
return if ::Feature.enabled?(:vue_merge_request_list, current_user)
|
||||
|
||||
render_merge_requests
|
||||
end
|
||||
def merge_requests; end
|
||||
|
||||
def projects
|
||||
@projects = @group.projects.with_statistics.page(params[:page])
|
||||
|
|
|
|||
|
|
@ -390,7 +390,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
private
|
||||
|
||||
def set_issuables_index
|
||||
return if ::Feature.enabled?(:vue_merge_request_list, current_user) && request.format.html?
|
||||
return if request.format.html?
|
||||
|
||||
super
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ module UserSettings
|
|||
def destroy
|
||||
# params[:id] can be an Rack::Session::SessionId#private_id
|
||||
ActiveSession.destroy_session(current_user, params[:id])
|
||||
current_user.forget_me!
|
||||
current_user.invalidate_all_remember_tokens!
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to user_settings_active_sessions_url, status: :found }
|
||||
|
|
|
|||
|
|
@ -42,6 +42,11 @@ module Packages
|
|||
|
||||
scope :for_package_type, ->(package_type) { where(package_type: package_type) }
|
||||
|
||||
def self.for_delete_exists?(access_level:, package_name:, package_type:)
|
||||
for_action_exists?(action: :delete, access_level: access_level, package_name: package_name,
|
||||
package_type: package_type)
|
||||
end
|
||||
|
||||
def self.for_action_exists?(action:, access_level:, package_name:, package_type:)
|
||||
return false if [access_level, package_name, package_type].any?(&:blank?)
|
||||
|
||||
|
|
|
|||
|
|
@ -1215,6 +1215,19 @@ class User < ApplicationRecord
|
|||
super if ::Gitlab::Database.read_write?
|
||||
end
|
||||
|
||||
# This is a copy of #forget_me! without the check for `expire_all_remember_me_on_sign_out`
|
||||
# https://github.com/heartcombo/devise/blob/v4.9.4/lib/devise/models/rememberable.rb#L58-L63
|
||||
#
|
||||
# We need a separate method because we disabled that setting but we also need to be able to
|
||||
# manually expire these tokens when a session is manually destroyed
|
||||
def invalidate_all_remember_tokens!
|
||||
return unless persisted?
|
||||
|
||||
self.remember_token = nil if respond_to?(:remember_token)
|
||||
self.remember_created_at = nil
|
||||
save(validate: false)
|
||||
end
|
||||
|
||||
# Override Devise Rememberable#remember_me?
|
||||
#
|
||||
# In Devise this method compares the remember me token received from the user session
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Packages
|
||||
module Protection
|
||||
class CheckDeleteRuleExistenceService < BaseProjectService
|
||||
SUCCESS_RESPONSE_RULE_EXISTS = ServiceResponse.success(payload: { protection_rule_exists?: true }).freeze
|
||||
SUCCESS_RESPONSE_RULE_DOESNT_EXIST = ServiceResponse.success(payload: { protection_rule_exists?: false }).freeze
|
||||
|
||||
ERROR_RESPONSE_UNAUTHORIZED = ServiceResponse.error(message: 'Unauthorized', reason: :unauthorized).freeze
|
||||
ERROR_RESPONSE_INVALID_PACKAGE_TYPE = ServiceResponse.error(message: 'Invalid package type',
|
||||
reason: :invalid_package_type).freeze
|
||||
|
||||
def execute
|
||||
return ERROR_RESPONSE_INVALID_PACKAGE_TYPE unless package_type_allowed?
|
||||
return ERROR_RESPONSE_UNAUTHORIZED unless current_user_can_destroy_package?
|
||||
return SUCCESS_RESPONSE_RULE_DOESNT_EXIST if current_user.can_admin_all_resources?
|
||||
|
||||
user_project_authorization_access_level = current_user.max_member_access_for_project(project.id)
|
||||
|
||||
response = project.package_protection_rules.for_delete_exists?(
|
||||
access_level: user_project_authorization_access_level,
|
||||
package_name: params[:package_name],
|
||||
package_type: params[:package_type]
|
||||
)
|
||||
|
||||
service_response_for(response)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def package_type_allowed?
|
||||
Packages::Protection::Rule.package_types.key?(params[:package_type])
|
||||
end
|
||||
|
||||
def current_user_can_destroy_package?
|
||||
can?(current_user, :destroy_package, project)
|
||||
end
|
||||
|
||||
def service_response_for(protection_rule_exists)
|
||||
protection_rule_exists ? SUCCESS_RESPONSE_RULE_EXISTS : SUCCESS_RESPONSE_RULE_DOESNT_EXIST
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -34,8 +34,7 @@ module Packages
|
|||
return false if current_user.can_admin_all_resources?
|
||||
|
||||
user_project_authorization_access_level = current_user.max_member_access_for_project(project.id)
|
||||
project.package_protection_rules
|
||||
.for_action_exists?(
|
||||
project.package_protection_rules.for_action_exists?(
|
||||
action: :push,
|
||||
access_level: user_project_authorization_access_level,
|
||||
package_name: params[:package_name],
|
||||
|
|
|
|||
|
|
@ -61,7 +61,8 @@
|
|||
5,
|
||||
6,
|
||||
7,
|
||||
8
|
||||
8,
|
||||
99
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,27 +2,6 @@
|
|||
- page_title _("Merge requests")
|
||||
- add_page_specific_style 'page_bundles/issuable_list'
|
||||
|
||||
- if Feature.enabled?(:vue_merge_request_list, @group)
|
||||
.js-merge-request-list-root{ data: group_merge_requests_list_data(@group, current_user) }
|
||||
- if has_bulk_update_permission
|
||||
= render_if_exists 'shared/issuable/group_bulk_update_sidebar', group: @group, type: :merge_requests
|
||||
- else
|
||||
- can_bulk_update = has_bulk_update_permission && issuables_count_for_state(:merge_requests, :all) > 0
|
||||
|
||||
.top-area
|
||||
= render 'shared/issuable/nav', type: :merge_requests, display_count: !@search_timeout_occurred
|
||||
- if current_user
|
||||
.nav-controls
|
||||
- if can_bulk_update
|
||||
= render_if_exists 'projects/merge_requests/bulk_update_button'
|
||||
|
||||
= render 'shared/new_project_item_vue_select'
|
||||
|
||||
= render 'shared/issuable/search_bar', type: :merge_requests
|
||||
- if can_bulk_update
|
||||
= render_if_exists 'shared/issuable/group_bulk_update_sidebar', group: @group, type: :merge_requests
|
||||
|
||||
- if @search_timeout_occurred
|
||||
= render 'shared/dashboard/search_timeout_occurred'
|
||||
- else
|
||||
= render 'shared/merge_requests'
|
||||
.js-merge-request-list-root{ data: group_merge_requests_list_data(@group, current_user) }
|
||||
- if has_bulk_update_permission
|
||||
= render_if_exists 'shared/issuable/group_bulk_update_sidebar', group: @group, type: :merge_requests
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
= render_if_exists 'projects/merge_requests/merge_trains_button'
|
||||
- if @can_bulk_update
|
||||
= render Pajamas::ButtonComponent.new(type: :submit, button_options: { class: 'gl-mr-3 js-bulk-update-toggle' }) do
|
||||
= _("Bulk edit")
|
||||
- if merge_project && can?(@current_user, :create_merge_request_in, @project)
|
||||
= render Pajamas::ButtonComponent.new(href: new_merge_request_path, variant: :confirm,
|
||||
button_options: { data: { event_tracking: 'click_new_merge_request_list' } }) do
|
||||
= _("New merge request")
|
||||
|
||||
#js-vue-mr-list-more-actions{ data: project_merge_requests_list_more_actions_data(@project, current_user) }
|
||||
|
|
@ -1,10 +1,6 @@
|
|||
- @can_bulk_update = can?(current_user, :admin_merge_request, @project)
|
||||
- merge_project = merge_request_source_project_for_project(@project)
|
||||
- new_merge_request_path = project_new_merge_request_path(merge_project) if merge_project
|
||||
- issuable_type = 'merge_request'
|
||||
|
||||
- page_title _("Merge requests")
|
||||
- new_merge_request_email = @project.new_issuable_address(current_user, 'merge_request')
|
||||
- add_page_specific_style 'page_bundles/issuable_list'
|
||||
- add_page_specific_style 'page_bundles/merge_request'
|
||||
|
||||
|
|
@ -13,27 +9,6 @@
|
|||
|
||||
= render 'projects/last_push'
|
||||
|
||||
- if Feature.enabled?(:vue_merge_request_list, @project)
|
||||
.js-merge-request-list-root{ data: project_merge_requests_list_data(@project, current_user) }
|
||||
- if @can_bulk_update
|
||||
= render 'shared/issuable/bulk_update_sidebar', type: :merge_requests
|
||||
- else
|
||||
- if @project.merge_requests.exists?
|
||||
.top-area
|
||||
= render 'shared/issuable/nav', type: :merge_requests
|
||||
.nav-controls
|
||||
= render "projects/merge_requests/nav_btns", merge_project: merge_project, new_merge_request_path: new_merge_request_path
|
||||
|
||||
= render 'shared/issuable/search_bar', type: :merge_requests
|
||||
|
||||
- if @can_bulk_update
|
||||
= render 'shared/issuable/bulk_update_sidebar', type: :merge_requests
|
||||
|
||||
.merge-requests-holder
|
||||
= render 'merge_requests', new_merge_request_path: new_merge_request_path
|
||||
- else
|
||||
= render 'shared/empty_states/merge_requests', button_path: new_merge_request_path
|
||||
|
||||
- if new_merge_request_email && can?(current_user, :create_merge_request_in, @project)
|
||||
.gl-text-center.gl-pt-5.gl-pb-7
|
||||
.js-issuable-by-email{ data: { initial_email: new_merge_request_email, issuable_type: issuable_type, emails_help_page_path: help_page_path('development/emails.md', anchor: 'email-namespace'), quick_actions_help_path: help_page_path('user/project/quick_actions.md'), markdown_help_path: help_page_path('user/markdown.md'), reset_path: new_issuable_address_project_path(@project, issuable_type: issuable_type) } }
|
||||
.js-merge-request-list-root{ data: project_merge_requests_list_data(@project, current_user) }
|
||||
- if @can_bulk_update
|
||||
= render 'shared/issuable/bulk_update_sidebar', type: :merge_requests
|
||||
|
|
|
|||
|
|
@ -11,11 +11,10 @@
|
|||
= _('Update selected')
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-bulk-update-menu-hide gl-float-right' }) do
|
||||
= _('Cancel')
|
||||
- if params[:state] != 'merged' || ::Feature.enabled?(:vue_merge_request_list, @project)
|
||||
.block.js-status-dropdown-container
|
||||
.title
|
||||
= _('Status')
|
||||
.js-status-dropdown
|
||||
.block.js-status-dropdown-container
|
||||
.title
|
||||
= _('Status')
|
||||
.js-status-dropdown
|
||||
.block
|
||||
.title
|
||||
= _('Assignee')
|
||||
|
|
|
|||
|
|
@ -101,19 +101,19 @@
|
|||
|
||||
= render_if_exists 'shared/projects/badges', project: project
|
||||
- if stars
|
||||
= link_to project_starrers_path(project), class: "#{css_metadata_classes} stars", title: _('Stars'), data: { container: 'body', placement: 'top' } do
|
||||
= link_to project_starrers_path(project), class: "#{css_metadata_classes} stars", title: _('Stars'), data: { container: 'body', placement: 'top' }, aria: { label: format(n_('%{project} has %{number} star', '%{project} has %{number} stars', project.star_count), number: project.star_count, project: project.name) } do
|
||||
= sprite_icon('star-o', size: 14, css_class: 'gl-mr-2')
|
||||
= badge_count(project.star_count)
|
||||
- if show_count?(disabled: !forks, compact_mode: compact_mode)
|
||||
= link_to project_forks_path(project), class: "#{css_metadata_classes} forks", title: _('Forks'), data: { container: 'body', placement: 'top' } do
|
||||
= link_to project_forks_path(project), class: "#{css_metadata_classes} forks", title: _('Forks'), data: { container: 'body', placement: 'top' }, aria: { label: format(n_('%{project} has %{number} fork', '%{project} has %{number} forks', project.forks_count), number: project.forks_count, project: project.name) } do
|
||||
= sprite_icon('fork', size: 14, css_class: 'gl-mr-2')
|
||||
= badge_count(project.forks_count)
|
||||
- if show_count?(disabled: !merge_requests, compact_mode: compact_mode)
|
||||
= link_to project_merge_requests_path(project), class: "#{css_metadata_classes} merge-requests", title: _('Merge requests'), data: { container: 'body', placement: 'top' } do
|
||||
= link_to project_merge_requests_path(project), class: "#{css_metadata_classes} merge-requests", title: _('Merge requests'), data: { container: 'body', placement: 'top' }, aria: { label: format(n_('%{project} has %{number} merge request', '%{project} has %{number} merge requests', project.open_merge_requests_count), number: project.open_merge_requests_count, project: project.name) } do
|
||||
= sprite_icon('merge-request', size: 14, css_class: 'gl-mr-2')
|
||||
= badge_count(project.open_merge_requests_count)
|
||||
- if show_count?(disabled: !issues, compact_mode: compact_mode)
|
||||
= link_to project_issues_path(project), class: "#{css_metadata_classes} issues", title: _('Issues'), data: { container: 'body', placement: 'top' } do
|
||||
= link_to project_issues_path(project), class: "#{css_metadata_classes} issues", title: _('Issues'), data: { container: 'body', placement: 'top' }, aria: { label: format(n_('%{project} has %{number} open issues', '%{project} has %{number} open issues', project.open_issues_count), number: project.open_issues_count, project: project.name) } do
|
||||
= sprite_icon('issues', size: 14, css_class: 'gl-mr-2')
|
||||
= badge_count(project.open_issues_count)
|
||||
= render_if_exists 'shared/projects/actions', project: project
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
name: vue_merge_request_list
|
||||
feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/10827
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/145168
|
||||
rollout_issue_url:
|
||||
milestone: '16.10'
|
||||
group: group::code review
|
||||
type: beta
|
||||
default_enabled: true
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
name: packages_protected_packages_delete
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323970
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/179931
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/516215
|
||||
milestone: '17.10'
|
||||
group: group::package registry
|
||||
type: gitlab_com_derisk
|
||||
default_enabled: false
|
||||
|
|
@ -116,15 +116,15 @@ Devise.setup do |config|
|
|||
# The time the user will be remembered without asking for credentials again.
|
||||
# config.remember_for = 2.weeks
|
||||
|
||||
# If true, a valid remember token can be re-used between multiple browsers.
|
||||
# config.remember_across_browsers = true
|
||||
# Invalidates all the remember me tokens when the user signs out.
|
||||
config.expire_all_remember_me_on_sign_out = false
|
||||
|
||||
# If true, extends the user's remember period when remembered via cookie.
|
||||
config.extend_remember_period = true
|
||||
|
||||
# Options to be passed to the created cookie. For instance, you can set
|
||||
# secure: true in order to force SSL only cookies.
|
||||
# config.cookie_options = {}
|
||||
# config.rememberable_options = {}
|
||||
|
||||
# When set to false, does not sign a user in automatically after their password is
|
||||
# changed. Defaults to true, so a user is signed in automatically after a password
|
||||
|
|
|
|||
|
|
@ -4,13 +4,24 @@ return if Gitlab::Utils.to_boolean(ENV['SKIP_CELL_CONFIG_VALIDATION'], default:
|
|||
|
||||
ValidationError = Class.new(StandardError)
|
||||
|
||||
print_error = ->(error_message) do
|
||||
message = error_message
|
||||
message += <<~MESSAGE if Gitlab.dev_or_test_env?
|
||||
|
||||
Make sure your development environment is up to date.
|
||||
For example, on GDK, run: gdk update
|
||||
MESSAGE
|
||||
|
||||
raise ValidationError, message
|
||||
end
|
||||
|
||||
if Gitlab.config.cell.enabled
|
||||
raise ValidationError, "Cell ID is not set to a valid positive integer" if Gitlab.config.cell.id.to_i < 1
|
||||
print_error.call("Cell ID is not set to a valid positive integer.") if Gitlab.config.cell.id.to_i < 1
|
||||
|
||||
Settings.topology_service_settings.each do |setting|
|
||||
setting_value = Gitlab.config.cell.topology_service_client.send(setting)
|
||||
raise ValidationError, "Topology Service setting '#{setting}' is not set" if setting_value.blank?
|
||||
print_error.call("Topology Service setting '#{setting}' is not set.") if setting_value.blank?
|
||||
end
|
||||
elsif Gitlab.config.cell.id.present?
|
||||
raise ValidationError, "Cell ID is set but Cell is not enabled"
|
||||
print_error.call("Cell ID is set but Cell is not enabled.")
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,41 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveBrokenFkA2141b1522P < Gitlab::Database::Migration[2.2]
|
||||
include Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers
|
||||
|
||||
milestone '17.10'
|
||||
disable_ddl_transaction!
|
||||
|
||||
SOURCE_TABLE_NAME = :p_ci_builds
|
||||
TARGET_TABLE_NAME = :p_ci_pipelines
|
||||
COLUMN = :auto_canceled_by_id
|
||||
TARGET_COLUMN = :id
|
||||
PARTITION_COLUMN = :auto_canceled_by_partition_id
|
||||
PARTITION_TARGET_COLUMN = :partition_id
|
||||
FK_NAME = :fk_a2141b1522_p
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
remove_foreign_key_if_exists(
|
||||
SOURCE_TABLE_NAME,
|
||||
TARGET_TABLE_NAME,
|
||||
name: FK_NAME,
|
||||
reverse_lock_order: true
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_partitioned_foreign_key(
|
||||
SOURCE_TABLE_NAME,
|
||||
TARGET_TABLE_NAME,
|
||||
column: [PARTITION_COLUMN, COLUMN],
|
||||
target_column: [PARTITION_TARGET_COLUMN, TARGET_COLUMN],
|
||||
reverse_lock_order: true,
|
||||
on_update: :cascade,
|
||||
on_delete: :nullify,
|
||||
name: FK_NAME,
|
||||
validate: true
|
||||
)
|
||||
end
|
||||
# No-op for https://gitlab.com/gitlab-com/gl-infra/production/-/issues/19464
|
||||
def up; end
|
||||
def down; end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -40484,6 +40484,9 @@ ALTER TABLE ONLY ml_candidates
|
|||
ALTER TABLE ONLY subscription_add_on_purchases
|
||||
ADD CONSTRAINT fk_a1db288990 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE p_ci_builds
|
||||
ADD CONSTRAINT fk_a2141b1522_p FOREIGN KEY (auto_canceled_by_partition_id, auto_canceled_by_id) REFERENCES p_ci_pipelines(partition_id, id) ON UPDATE CASCADE ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY protected_environment_approval_rules
|
||||
ADD CONSTRAINT fk_a3cc825836 FOREIGN KEY (protected_environment_project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
|||
|
|
@ -40892,6 +40892,7 @@ AI features that can be configured in the settings.
|
|||
| <a id="aifeaturesduo_chat_explain_code"></a>`DUO_CHAT_EXPLAIN_CODE` | Duo chat explain code feature setting. |
|
||||
| <a id="aifeaturesduo_chat_fix_code"></a>`DUO_CHAT_FIX_CODE` | Duo chat fix code feature setting. |
|
||||
| <a id="aifeaturesduo_chat_refactor_code"></a>`DUO_CHAT_REFACTOR_CODE` | Duo chat refactor code feature setting. |
|
||||
| <a id="aifeaturesduo_chat_troubleshoot_job"></a>`DUO_CHAT_TROUBLESHOOT_JOB` | Duo chat troubleshoot job feature setting. |
|
||||
| <a id="aifeaturesduo_chat_write_tests"></a>`DUO_CHAT_WRITE_TESTS` | Duo chat write test feature setting. |
|
||||
|
||||
### `AiMessageRole`
|
||||
|
|
|
|||
|
|
@ -433,12 +433,15 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git
|
|||
|
||||
Can return the following status codes:
|
||||
|
||||
- `204 No Content`, if the package was deleted successfully.
|
||||
- `404 Not Found`, if the package was not found.
|
||||
- `204 No Content`: The package was deleted successfully.
|
||||
- `403 Forbidden`: The package is protected from deletion.
|
||||
- `404 Not Found`: The package was not found.
|
||||
|
||||
If [request forwarding](../user/packages/package_registry/supported_functionality.md#forwarding-requests) is enabled,
|
||||
deleting a package can introduce a [dependency confusion risk](../user/packages/package_registry/supported_functionality.md#deleting-packages).
|
||||
|
||||
If a package is protected by a [protection rule](../user/packages/package_registry/package_protection_rules.md#protect-a-package), then deleting the package is forbidden.
|
||||
|
||||
## Delete a package file
|
||||
|
||||
{{< alert type="warning" >}}
|
||||
|
|
@ -467,5 +470,7 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git
|
|||
Can return the following status codes:
|
||||
|
||||
- `204 No Content`: The package was deleted successfully.
|
||||
- `403 Forbidden`: The user does not have permission to delete the file.
|
||||
- `403 Forbidden`: The user does not have permission to delete the file or the package is protected from deletion.
|
||||
- `404 Not Found`: The package or package file was not found.
|
||||
|
||||
If a package that a package file belongs to is protected by a [protection rule](../user/packages/package_registry/package_protection_rules.md#protect-a-package), then deleting the package file is forbidden.
|
||||
|
|
|
|||
|
|
@ -282,6 +282,8 @@ build_submodule:
|
|||
- unzip artifacts.zip
|
||||
```
|
||||
|
||||
To fetch artifacts from a job in the same pipeline, use the [`needs:artifacts`](../yaml/_index.md#needsartifacts) keyword.
|
||||
|
||||
## Browse the contents of the artifacts archive
|
||||
|
||||
You can browse the contents of the artifacts from the UI without downloading the artifact locally,
|
||||
|
|
|
|||
|
|
@ -739,8 +739,13 @@ subcommand. However, `GIT_SUBMODULE_UPDATE_FLAGS` flags are appended after a few
|
|||
Git honors the last occurrence of a flag in the list of arguments, so manually
|
||||
providing them in `GIT_SUBMODULE_UPDATE_FLAGS` overrides these default flags.
|
||||
|
||||
You can use this variable to fetch the latest remote `HEAD` instead of the tracked commit in the repository.
|
||||
You can also use it to speed up the checkout by fetching submodules in multiple parallel jobs.
|
||||
For example, you can use this variable to:
|
||||
|
||||
- Fetch the latest remote `HEAD` instead of the tracked commit in the
|
||||
repository (default) to automatically updated all submodules with the
|
||||
`--remote` flag.
|
||||
- Speed up the checkout by fetching submodules in multiple parallel jobs with
|
||||
the `--jobs 4` flag.
|
||||
|
||||
```yaml
|
||||
variables:
|
||||
|
|
@ -758,12 +763,35 @@ git submodule update --init --depth 50 --recursive --remote --jobs 4
|
|||
|
||||
{{< alert type="warning" >}}
|
||||
|
||||
You should be aware of the implications for the security, stability, and reproducibility of
|
||||
your builds when using the `--remote` flag. In most cases, it is better to explicitly track
|
||||
submodule commits as designed, and update them using an auto-remediation/dependency bot.
|
||||
You should be aware of the implications for the security, stability, and
|
||||
reproducibility of your builds when using the `--remote` flag. In most cases,
|
||||
it is better to explicitly track submodule commits as designed, and update them
|
||||
using an auto-remediation/dependency bot.
|
||||
|
||||
The `--remote` flag is not required to check out submodules at their committed
|
||||
revisions. Use this flag only when you want to automatically updated submodules
|
||||
to their latest remote versions.
|
||||
|
||||
{{< /alert >}}
|
||||
|
||||
The behavior of `--remote` depends on your Git version. Some Git versions might
|
||||
fail, with the error below, when the branch in the superproject's `.gitmodules`
|
||||
differs from the default branch of the submodule repository:
|
||||
|
||||
`fatal: Unable to find refs/remotes/origin/<branch> revision in submodule path '<submodule-path>'`
|
||||
|
||||
The runner implements a "best effort" fallback that attempts to
|
||||
pull remote refs when the submodule update fails.
|
||||
|
||||
If this fallback does not work with your Git version, try one of the following
|
||||
workarounds:
|
||||
|
||||
- Update the submodule repository's default branch to match the branch set in
|
||||
`.gitmodules` in the superproject.
|
||||
- Set `GIT_SUBMODULE_DEPTH` to `0`.
|
||||
- Update the submodules separately and remove the `--remote` flag from
|
||||
`GIT_SUBMODULE_UPDATE_FLAGS`.
|
||||
|
||||
### Rewrite submodule URLs to HTTPS
|
||||
|
||||
{{< history >}}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ includes a task or reference topic.
|
|||
The tech writing team sometimes uses the acronym `CTRT` to refer to the topic types.
|
||||
The acronym refers to the first letter of each topic type.
|
||||
|
||||
<i class="fa-youtube-play" aria-hidden="true"></i>
|
||||
For an overview, see [Editing for style and topic type](https://youtu.be/HehnjPgPWb0).
|
||||
<!-- Video published on 2021-06-06 -->
|
||||
|
||||
## Other page and topic types
|
||||
|
||||
In addition to the four primary topic types, you can use the following:
|
||||
|
|
|
|||
|
|
@ -59,3 +59,24 @@ Avoid these topic titles:
|
|||
noun or phrase that someone would search for.
|
||||
- `Use cases`. Instead, incorporate the information as part of the concept.
|
||||
- `How it works`. Instead, use a noun followed by `workflow`. For example, `Merge request workflow`.
|
||||
|
||||
## Example
|
||||
|
||||
### Before
|
||||
|
||||
The following topic was trying to be all things to all people. It provided information about groups
|
||||
and where to find them. It reiterated what was visible in the UI.
|
||||
|
||||

|
||||
|
||||
### After
|
||||
|
||||
The information is easier to scan if you move it into concepts and [tasks](task.md).
|
||||
|
||||
#### Concept
|
||||
|
||||

|
||||
|
||||
#### Task
|
||||
|
||||

|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
|
|
@ -34,3 +34,18 @@ Avoid these topic titles:
|
|||
for a task, or information about a concept.
|
||||
- `Limitations`. Instead, move the content near other similar information.
|
||||
If you must, you can use the title `Known issues`.
|
||||
|
||||
## Example
|
||||
|
||||
### Before
|
||||
|
||||
This topic was a compilation of a variety of information and was difficult to scan.
|
||||
|
||||

|
||||
|
||||
### After
|
||||
|
||||
The information in the **Overview** topic is now organized in a table
|
||||
that's easy to scan. It also has a more searchable title.
|
||||
|
||||

|
||||
|
|
|
|||
|
|
@ -163,3 +163,4 @@ how to write the phrase for each role.
|
|||
## Related topics
|
||||
|
||||
- [How to write task steps](../styleguide/_index.md#navigation)
|
||||
- [Before and after example](concept.md#example)
|
||||
|
|
|
|||
|
|
@ -435,14 +435,37 @@ scope2 = User.select(*columns).where(id: [10, 11, 12]) # uses SELECT users.*
|
|||
User.connection.execute(Gitlab::SQL::Union.new([scope1, scope2]).to_sql)
|
||||
```
|
||||
|
||||
## Ordering by Creation Date
|
||||
## Ordering by Creation Date (`created_at`)
|
||||
|
||||
When ordering records based on the time they were created, you can order
|
||||
by the `id` column instead of ordering by `created_at`. Because IDs are always
|
||||
unique and incremented in the order that rows are created, doing so produces the
|
||||
exact same results. This also means there's no need to add an index on
|
||||
`created_at` to ensure consistent performance as `id` is already indexed by
|
||||
default.
|
||||
In short, you should prefer `ORDER BY id` over `ORDER BY created_at` unless you
|
||||
are sure it is going to cause problems for your feature.
|
||||
|
||||
There is a common user facing desire to provide data that is sorted by
|
||||
`created_at`. It's common in paginated table views and paginated APIs to want
|
||||
to see the most recent first (or oldest first). This usually results in us
|
||||
wanting to add something like `ORDER BY created_at DESC LIMIT 20` to our
|
||||
queries. Adding this query would mean that we need to add an index on
|
||||
`created_at` (or composite index depending on the other filtering
|
||||
requirements). Adding indexes comes with
|
||||
[a cost](database/adding_database_indexes.md#maintenance-overhead).
|
||||
Furthermore, since `created_at` usually isn't a unique column then sorting
|
||||
and paginating over it would be unstable and we'd still need to add a
|
||||
[tie-breaker column to the sort](database/pagination_performance_guidelines.md#tie-breaker-column)
|
||||
(e.g. `ORDER BY created_at, id`) with an appropriate index for that.
|
||||
|
||||
But, for the majority of features our users find that `ORDER BY id` is a good
|
||||
enough proxy for what they need. It's not technically always
|
||||
true that ordering by `id` is exactly the same as ordering by `created_at` but
|
||||
it is close enough and considering that `created_at` is almost never controlled
|
||||
directly by users (ie. it's an internal implementation detail), then there is
|
||||
rarely a case where the user actually cares about the difference between these
|
||||
2 columns.
|
||||
|
||||
So there are at least 3 advantages to ordering by `id`:
|
||||
|
||||
1. As a primary key, it is already indexed, which may be sufficient for simple queries that don't have other filtering or sorting parameters.
|
||||
1. If a composite index is required, indexes such as `btree (namespace_id, id)` are smaller than `btree (namespace_id, created_at, id)`.
|
||||
1. It is unique and thus stable for sorting and paginating.
|
||||
|
||||
## Use `WHERE EXISTS` instead of `WHERE IN`
|
||||
|
||||
|
|
|
|||
|
|
@ -145,3 +145,78 @@ dast:
|
|||
Adjusting these values may impact scan time because they adjust how long each browser waits for various activities to complete.
|
||||
|
||||
{{< /alert >}}
|
||||
|
||||
### Page readiness timeouts
|
||||
|
||||
Page readiness refers to the state when a page has loaded completely, its DOM has stabilized, and interactive elements are available. Proper page readiness detection is crucial for:
|
||||
|
||||
- **Scanning accuracy**: Analyzing pages before they're fully loaded can miss content or produce false negatives.
|
||||
- **Crawl efficiency**: Waiting too long wastes scanning time, while not waiting enough misses dynamic content.
|
||||
- **Modern web application support**: Single-page applications, AJAX-heavy sites, and progressive loading patterns require sophisticated readiness detection.
|
||||
|
||||
Using a sequence of optional configurable timeouts, the DAST scanner can detect when different parts of a page have loaded completely.
|
||||
|
||||
#### Timeout variables
|
||||
|
||||
Use the following CI/CD variables to customize DAST page readiness timeouts.
|
||||
For a comprehensive list, see [Available CI/CD variables](variables.md).
|
||||
|
||||
| Timeout Variable | Default | Description |
|
||||
|:-----------------|:--------|:------------|
|
||||
| `DAST_PAGE_READY_AFTER_NAVIGATION_TIMEOUT` | `15s` | The maximum amount of time to wait for a browser to navigate from one page to another. Used during the Document Load phase for full page loads. |
|
||||
| `DAST_PAGE_READY_AFTER_ACTION_TIMEOUT` | `7s` | The maximum amount of time to wait for a browser to consider a page loaded and ready for analysis. Used as an alternative to `DAST_PAGE_READY_AFTER_NAVIGATION_TIMEOUT` for in-page actions that don't trigger a full page load. |
|
||||
| `DAST_PAGE_DOM_STABLE_WAIT` | `500ms` | Define how long to wait for updates to the DOM before checking a page is stable. Used at the beginning of the client-side render phase. |
|
||||
| `DAST_PAGE_DOM_READY_TIMEOUT` | `6s` | The maximum amount of time to wait for a browser to consider a page loaded and ready for analysis after a navigation completes. Controls waiting for background data fetching and DOM rendering. |
|
||||
| `DAST_PAGE_IS_LOADING_ELEMENT` | None | Selector that when no longer visible on the page, indicates to the analyzer that the page has finished loading and the scan can continue. Marks the end of the client-side render process. |
|
||||
|
||||
#### Page loading workflow
|
||||
|
||||
Modern web applications load in multiple stages. The DAST scanner has specific timeouts for
|
||||
each step in the process:
|
||||
|
||||
1. **Document loading**: The browser fetches and processes the basic page structure.
|
||||
|
||||
1. Fetch HTML content from the server.
|
||||
1. Load referenced CSS and JavaScript files.
|
||||
1. Parse content and renders the initial page.
|
||||
1. Trigger the standard "document ready" event.
|
||||
|
||||
This phase uses either `DAST_PAGE_READY_AFTER_NAVIGATION_TIMEOUT` (for full page loads) or `DAST_PAGE_READY_AFTER_ACTION_TIMEOUT` (for in-page actions), which sets the maximum wait time for document loading.
|
||||
|
||||
1. **Client-Side rendering**: After initial loading, many single-page applications:
|
||||
|
||||
- Perform initial JavaScript execution (`DAST_PAGE_DOM_STABLE_WAIT`).
|
||||
- Fetch background data with AJAX or other API calls.
|
||||
- Render a DOM and performs updates based on fetched data (`DAST_PAGE_DOM_READY_TIMEOUT`).
|
||||
- Display page loading indicators (`DAST_PAGE_IS_LOADING_ELEMENT`).
|
||||
|
||||
The scanner monitors these activities to determine when the page is ready for interaction.
|
||||
|
||||
The following chart illustrates the sequence timeouts used when crawling a page:
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"gantt": {
|
||||
"leftPadding": 250,
|
||||
"sectionFontSize": 15,
|
||||
"topPadding": 40,
|
||||
"fontFamily": "GitLab Sans"
|
||||
}
|
||||
}}%%
|
||||
gantt
|
||||
dateFormat YYYY-MM-DD
|
||||
axisFormat
|
||||
section Document load
|
||||
DAST_PAGE_READY_AFTER_NAVIGATION_TIMEOUT :done, nav1, 2024-01-01, 6d
|
||||
Fetch HTML :active, nav1, 2024-01-01, 3d
|
||||
Fetch CSS&JS :active, nav1, 2024-01-04, 3d
|
||||
DocumentReady :milestone, nav1, 2024-01-07, 0d
|
||||
|
||||
section Load Data / Client-side render
|
||||
DAST_PAGE_DOM_STABLE_WAIT :done, dom1, 2024-01-07, 3d
|
||||
Initial JS Execution :active, dom1, 2024-01-07, 3d
|
||||
DAST_PAGE_DOM_READY_TIMEOUT :done, ready1, 2024-01-10, 4d
|
||||
Fetch Data :active, dom1, 2024-01-10, 2d
|
||||
Render DOM :active, dom1, 2024-01-10, 2d
|
||||
DAST_PAGE_IS_LOADING_ELEMENT :milestone, load1, 2024-01-14, 0d
|
||||
```
|
||||
|
|
|
|||
|
|
@ -302,6 +302,7 @@ refreshed.
|
|||
{{< history >}}
|
||||
|
||||
- [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/16157) in GitLab 17.9 [with a flag](../../../administration/feature_flags.md) named `vulnerability_severity_override`. Disabled by default.
|
||||
- [Enabled on GitLab.com, GitLab Self-Managed, and GitLab Dedicated](https://issue-link) in GitLab 17.10.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
|
|
@ -309,7 +310,6 @@ refreshed.
|
|||
|
||||
The availability of this feature is controlled by a feature flag.
|
||||
For more information, see the history.
|
||||
This feature is available for testing, but not ready for production use.
|
||||
|
||||
{{< /alert >}}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,169 @@
|
|||
---
|
||||
stage: AI-powered
|
||||
group: AI Framework
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
description: "Learn how to use GitLab Duo AI-powered features to enhance your software development lifecycle."
|
||||
title: 'GitLab Duo: Choose your path'
|
||||
---
|
||||
|
||||
GitLab Duo is a suite of AI-powered features that assist you while you work in GitLab.
|
||||
|
||||
Select the path that best matches what you want to do:
|
||||
|
||||
{{< tabs >}}
|
||||
|
||||
{{< tab title="Get started" >}}
|
||||
|
||||
**Perfect for**: New users exploring GitLab Duo
|
||||
|
||||
Follow this path to learn how to:
|
||||
|
||||
- Use the variety of GitLab Duo features
|
||||
- Get help from AI through GitLab Duo Chat
|
||||
- Generate and improve code
|
||||
|
||||
[Start here: GitLab Duo →](_index.md)
|
||||
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab title="Enhance my coding" >}}
|
||||
|
||||
**Perfect for**: Developers looking to boost productivity
|
||||
|
||||
Follow this path to learn how to:
|
||||
|
||||
- Use Code Suggestions in your IDE
|
||||
- Generate, understand, and refactor code
|
||||
- Create tests automatically
|
||||
|
||||
[Start here: Code Suggestions →](../../user/project/repository/code_suggestions/_index.md)
|
||||
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab title="Improve code reviews" >}}
|
||||
|
||||
**Perfect for**: Reviewers and team leads
|
||||
|
||||
Follow this path to learn how to:
|
||||
|
||||
- Generate merge request descriptions
|
||||
- Get AI-powered code reviews
|
||||
- Summarize review comments and generate commit messages
|
||||
|
||||
[Start here: GitLab Duo in merge requests →](../../user/project/merge_requests/duo_in_merge_requests.md)
|
||||
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab title="Secure my application" >}}
|
||||
|
||||
**Perfect for**: Security and DevSecOps professionals
|
||||
|
||||
Follow this path to learn how to:
|
||||
|
||||
- Understand vulnerabilities
|
||||
- Automatically generate fix suggestions
|
||||
- Create merge requests to address security issues
|
||||
|
||||
[Start here: Vulnerability explanation and resolution →](../application_security/vulnerabilities/_index.md#explaining-a-vulnerability)
|
||||
|
||||
{{< /tab >}}
|
||||
|
||||
{{< /tabs >}}
|
||||
|
||||
## Quick start
|
||||
|
||||
Want to start using GitLab Duo right now? Here's how:
|
||||
|
||||
1. Open GitLab Duo Chat by selecting **GitLab Duo Chat** in the upper-right corner of the GitLab UI.
|
||||
1. Ask a question about your project, code, or how to use GitLab.
|
||||
1. Try one of the AI-powered features like Code Suggestions in your IDE, or use Chat to summarize a bulky issue.
|
||||
|
||||
[View all of the GitLab Duo possibilities →](_index.md)
|
||||
|
||||
## Common tasks
|
||||
|
||||
Need to do something specific? Here are some common tasks:
|
||||
|
||||
| Task | Description | Quick Guide |
|
||||
|------|-------------|-------------|
|
||||
| Get AI assistance | Ask GitLab Duo questions about code, projects, or GitLab | [GitLab Duo Chat →](../gitlab_duo_chat/_index.md) |
|
||||
| Generate code | Get code suggestions as you type in your IDE | [Code Suggestions →](../../user/project/repository/code_suggestions/_index.md) |
|
||||
| Understand code | Have code explained in plain language | [Code Explanation →](../../user/project/repository/code_explain.md) |
|
||||
| Fix CI/CD issues | Analyze and fix failed jobs | [Root Cause Analysis →](../gitlab_duo_chat/examples.md#troubleshoot-failed-cicd-jobs-with-root-cause-analysis) |
|
||||
| Summarize changes | Generate descriptions for merge requests | [Merge Request Summary →](../../user/project/merge_requests/duo_in_merge_requests.md#generate-a-description-by-summarizing-code-changes) |
|
||||
|
||||
## How GitLab Duo integrates with your workflow
|
||||
|
||||
GitLab Duo is integrated with your development processes and is available:
|
||||
|
||||
- In the GitLab UI
|
||||
- Through GitLab Duo Chat
|
||||
- In IDE extensions
|
||||
- In the CLI
|
||||
|
||||
## Experience levels
|
||||
|
||||
### For beginners
|
||||
|
||||
If you're new to GitLab Duo, start with these features:
|
||||
|
||||
- **[GitLab Duo Chat](../gitlab_duo_chat/_index.md)** - Ask questions about GitLab and get help with basic tasks
|
||||
- **[Code Explanation](../../user/project/repository/code_explain.md)** - Understand code in files or merge requests
|
||||
- **[Merge Request Summary](../../user/project/merge_requests/duo_in_merge_requests.md#generate-a-description-by-summarizing-code-changes)** - Generate descriptions for your changes automatically
|
||||
|
||||
### For intermediate users
|
||||
|
||||
After you're comfortable with the basics, try these more advanced features:
|
||||
|
||||
- **[Code Suggestions](../../user/project/repository/code_suggestions/_index.md)** - Get AI-powered code completion in your IDE
|
||||
- **[Test Generation](../gitlab_duo_chat/examples.md#write-tests-in-the-ide)** - Create tests for your code automatically
|
||||
- **[Root Cause Analysis](../gitlab_duo_chat/examples.md#troubleshoot-failed-cicd-jobs-with-root-cause-analysis)** - Troubleshoot failed CI/CD jobs
|
||||
|
||||
### For advanced users
|
||||
|
||||
When you're ready to maximize your productivity with GitLab Duo:
|
||||
|
||||
- **[GitLab Duo Self-Hosted](../../administration/gitlab_duo_self_hosted/_index.md)** - Host LLMs on your own infrastructure
|
||||
- **[GitLab Duo Workflow](../duo_workflow/_index.md)** - Automate tasks in your development workflow
|
||||
- **[Vulnerability Resolution](../../user/application_security/vulnerabilities/_index.md#vulnerability-resolution)** - Automatically generate merge requests to fix security issues
|
||||
|
||||
## Best practices
|
||||
|
||||
Follow these tips for effective GitLab Duo usage:
|
||||
|
||||
1. **Be specific in your prompts**
|
||||
- Provide clear context for better results
|
||||
- Include relevant details about your code and objectives
|
||||
- Use code task commands like `/explain`, `/refactor`, and `/tests` in Chat
|
||||
|
||||
1. **Improve code responsibly**
|
||||
- Always review AI-generated code before using it
|
||||
- Test generated code to ensure it works as expected
|
||||
- Use vulnerability resolution with appropriate review
|
||||
|
||||
1. **Refine iteratively**
|
||||
- If a response isn't helpful, refine your question
|
||||
- Try breaking complex requests into smaller parts
|
||||
- Add more details for better context
|
||||
|
||||
1. **Leverage Chat for learning**
|
||||
- Ask about GitLab features you're not familiar with
|
||||
- Get explanations for error messages and problems
|
||||
- Learn best practices for your specific technology
|
||||
|
||||
## Next steps
|
||||
|
||||
Ready to dive deeper? Try these resources:
|
||||
|
||||
- [GitLab Duo use cases](use_cases.md) - Practical examples and exercises
|
||||
- [Set up GitLab Duo Self-Hosted](../../administration/gitlab_duo_self_hosted/_index.md) - For complete control over your data
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Having issues? Check these common solutions:
|
||||
|
||||
- [GitLab Duo features don't work on self-managed](troubleshooting.md#gitlab-duo-features-do-not-work-on-self-managed)
|
||||
- [GitLab Duo features not available for users](troubleshooting.md#gitlab-duo-features-not-available-for-users)
|
||||
- [Run a health check](setup.md#run-a-health-check-for-gitlab-duo) to diagnose your GitLab Duo setup
|
||||
|
||||
Need more help? Search the GitLab documentation or [ask the GitLab community](https://forum.gitlab.com/).
|
||||
|
|
@ -18,6 +18,10 @@ module API
|
|||
documentation: { example: '2021-01-01' }
|
||||
optional :last_used_after, type: DateTime, desc: 'Filter tokens which were used after given datetime',
|
||||
documentation: { example: '2022-01-01' }
|
||||
optional :expires_before, type: Date, desc: 'Filter tokens which expire before given datetime',
|
||||
documentation: { example: '2022-01-01' }
|
||||
optional :expires_after, type: Date, desc: 'Filter tokens which expire after given datetime',
|
||||
documentation: { example: '2021-01-01' }
|
||||
optional :search, type: String, desc: 'Filters tokens by name', documentation: { example: 'token' }
|
||||
optional :sort, type: String, desc: 'Sort tokens', documentation: { example: 'created_at_desc' }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -67,6 +67,17 @@ module API
|
|||
|
||||
not_found! unless package
|
||||
|
||||
if Feature.enabled?(:packages_protected_packages_delete, user_project)
|
||||
service_response =
|
||||
Packages::Protection::CheckDeleteRuleExistenceService.new(
|
||||
project: user_project,
|
||||
current_user: current_user,
|
||||
params: { package_name: package.name, package_type: package.package_type }
|
||||
).execute
|
||||
|
||||
forbidden!('Package is deletion protected.') if service_response[:protection_rule_exists?]
|
||||
end
|
||||
|
||||
package_file = package.installable_package_files
|
||||
.find_by_id(params[:package_file_id])
|
||||
|
||||
|
|
|
|||
|
|
@ -139,6 +139,17 @@ module API
|
|||
delete ':id/packages/:package_id' do
|
||||
authorize_destroy_package!(user_project)
|
||||
|
||||
if Feature.enabled?(:packages_protected_packages_delete, user_project)
|
||||
service_response =
|
||||
Packages::Protection::CheckDeleteRuleExistenceService.new(
|
||||
project: user_project,
|
||||
current_user: current_user,
|
||||
params: { package_name: package.name, package_type: package.package_type }
|
||||
).execute
|
||||
|
||||
forbidden!('Package is deletion protected.') if service_response[:protection_rule_exists?]
|
||||
end
|
||||
|
||||
destroy_conditionally!(package) do |package|
|
||||
::Packages::MarkPackageForDestructionService.new(container: package, current_user: current_user).execute
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ module API
|
|||
|
||||
feature_category :system_access
|
||||
|
||||
helpers ::API::Helpers::PersonalAccessTokensHelpers
|
||||
|
||||
%w[project group].each do |source_type|
|
||||
resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
|
||||
desc 'Get list of all access tokens for the specified resource' do
|
||||
|
|
@ -20,15 +22,14 @@ module API
|
|||
end
|
||||
params do
|
||||
requires :id, types: [String, Integer], desc: "ID or URL-encoded path of the #{source_type}"
|
||||
optional :state, type: String, desc: 'Filter tokens which are either active or inactive',
|
||||
values: %w[active inactive], documentation: { example: 'active' }
|
||||
use :access_token_params
|
||||
end
|
||||
get ":id/access_tokens" do
|
||||
resource = find_source(source_type, params[:id])
|
||||
|
||||
next unauthorized! unless current_user.can?(:read_resource_access_tokens, resource)
|
||||
|
||||
tokens = PersonalAccessTokensFinder.new({ user: resource.bots, impersonation: false, state: params[:state] }).execute.preload_users
|
||||
tokens = PersonalAccessTokensFinder.new(declared(params, include_missing: false).merge({ user: resource.bots, impersonation: false })).execute.preload_users
|
||||
|
||||
resource.members.load
|
||||
present paginate(tokens), with: Entities::ResourceAccessToken, resource: resource
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ module Gitlab
|
|||
track_analytics_event(event_name, send_snowplow_event, category: category,
|
||||
additional_properties: additional_properties, **kwargs)
|
||||
|
||||
return unless event_definition
|
||||
|
||||
kwargs[:additional_properties] = additional_properties
|
||||
event_definition.extra_tracking_classes.each do |tracking_class|
|
||||
tracking_class.track_event(event_name, **kwargs)
|
||||
|
|
|
|||
|
|
@ -36962,9 +36962,32 @@ msgstr ""
|
|||
msgid "MissingSSHKeyWarningLink|You won't be able to pull or push repositories via SSH until you add an SSH key to your profile"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|%d experiment"
|
||||
msgid_plural "MlExperimentTracking|%d experiments"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "MlExperimentTracking|(Optional)"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Artifacts"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|CI Info"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Complete the form below to promote run to a model version."
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Create"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Create an experiment using MLflow"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Create experiments using MLflow"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Creating an experiment"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -36989,9 +37012,24 @@ msgstr ""
|
|||
msgid "MlExperimentTracking|Deleting this run will delete the associated parameters, metrics, and metadata."
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Description"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Details & Metadata"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Download as CSV"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Enter a model version description"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Enter a semantic version."
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Enter some description"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Experiment created %{timeAgo} by %{author}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -37007,9 +37045,15 @@ msgstr ""
|
|||
msgid "MlExperimentTracking|Failed to load experiment candidates with error: %{message}"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Failed to load experiments with error: %{error}"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Failed to remove run"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|For example 1.0.0"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Get started with model experiments!"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -37019,39 +37063,93 @@ msgstr ""
|
|||
msgid "MlExperimentTracking|Last activity"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|MLflow run ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Metric"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Model"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Model experiments"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Must be a semantic version."
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Must be a semantic version. Latest version is %{latestVersion}"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Name"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|No candidates associated with this experiment"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|No logged artifacts."
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|No logged experiment metadata"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|No logged metadata"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|No logged metrics"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|No logged parameters"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|No results"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Number of runs"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Overview"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Parameters"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Performance"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Promote"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Promote run"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Run %{id}"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Run not linked to a CI build"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Run removed"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Runs"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Select a model"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Select the model that will contain the new version. The run will move to the default experiment of that model."
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Step %{step}"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|To learn more about MLflow client compatibility, see %{linkStart}the documentation%{linkEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Triggered by"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Use candidates to track performance, parameters, and metadata"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -37061,13 +37159,17 @@ msgstr ""
|
|||
msgid "MlExperimentTracking|Value"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|by %{author}"
|
||||
msgid "MlExperimentTracking|Version"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|%d experiment"
|
||||
msgid_plural "MlModelRegistry|%d experiments"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
msgid "MlExperimentTracking|Version is not a valid semantic version."
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|Version is valid semantic version."
|
||||
msgstr ""
|
||||
|
||||
msgid "MlExperimentTracking|by %{author}"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|%d model"
|
||||
msgid_plural "MlModelRegistry|%d models"
|
||||
|
|
@ -37109,18 +37211,12 @@ msgstr ""
|
|||
msgid "MlModelRegistry|CI Info"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Complete the form below to promote run to a model version."
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Create"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Create & import"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Create experiments using MLflow"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Create model"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -37178,9 +37274,6 @@ msgstr ""
|
|||
msgid "MlModelRegistry|Description"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Details & Metadata"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Drop or %{linkStart}select%{linkEnd} artifacts to attach"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -37223,9 +37316,6 @@ msgstr ""
|
|||
msgid "MlModelRegistry|Failed to delete model with error: %{message}"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Failed to load experiments with error: %{error}"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Failed to load model runs with error: %{message}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -37274,12 +37364,6 @@ msgstr ""
|
|||
msgid "MlModelRegistry|Metadata"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Metric"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Model"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Model card"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -37325,9 +37409,6 @@ msgstr ""
|
|||
msgid "MlModelRegistry|No description available. To add a description, click \"Edit model\" above."
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|No logged artifacts."
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|No logged metadata"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -37337,9 +37418,6 @@ msgstr ""
|
|||
msgid "MlModelRegistry|No logged parameters"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|No results"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|No runs associated with this model"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -37352,12 +37430,6 @@ msgstr ""
|
|||
msgid "MlModelRegistry|Performance"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Promote"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Promote run"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Provide a subfolder name to organize your artifacts. Entering an existing subfolder's name will place artifacts in the existing folder"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -37376,21 +37448,12 @@ msgstr ""
|
|||
msgid "MlModelRegistry|Save changes"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Select a model"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Select the model that will contain the new version. The run will move to the default experiment of that model."
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Setting up the client"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Something went wrong while trying to delete the model version. Please try again later."
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Step %{step}"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Subfolder"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -43467,7 +43530,7 @@ msgstr ""
|
|||
msgid "Pipeline|Only the first 100 jobs per stage are displayed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|Other"
|
||||
msgid "Pipeline|Other (Cancelled, Skipped)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|Passed"
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@
|
|||
"@gitlab/fonts": "^1.3.0",
|
||||
"@gitlab/query-language-rust": "0.4.0",
|
||||
"@gitlab/svgs": "3.123.0",
|
||||
"@gitlab/ui": "110.0.0",
|
||||
"@gitlab/ui": "110.1.0",
|
||||
"@gitlab/vue-router-vue3": "npm:vue-router@4.5.0",
|
||||
"@gitlab/vuex-vue3": "npm:vuex@4.1.0",
|
||||
"@gitlab/web-ide": "^0.0.1-dev-20250309164831",
|
||||
|
|
|
|||
|
|
@ -195,7 +195,6 @@ spec/frontend/projects/settings_service_desk/components/custom_email_form_spec.j
|
|||
spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
|
||||
spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
|
||||
spec/frontend/ref/init_ambiguous_ref_modal_spec.js
|
||||
spec/frontend/releases/components/app_edit_new_spec.js
|
||||
spec/frontend/releases/components/asset_links_form_spec.js
|
||||
spec/frontend/repository/components/table/index_spec.js
|
||||
spec/frontend/repository/components/table/row_spec.js
|
||||
|
|
|
|||
|
|
@ -523,92 +523,6 @@ RSpec.describe GroupsController, :with_current_organization, factory_default: :k
|
|||
|
||||
expect(response).to render_template('groups/merge_requests')
|
||||
end
|
||||
|
||||
context 'sorting by votes' do
|
||||
context 'when vue_merge_request_list is disabled' do
|
||||
before do
|
||||
stub_feature_flags(vue_merge_request_list: false)
|
||||
end
|
||||
|
||||
it 'sorts most popular merge requests' do
|
||||
get :merge_requests, params: { id: group.to_param, sort: 'upvotes_desc' }
|
||||
expect(assigns(:merge_requests)).to eq [merge_request_2, merge_request_1]
|
||||
end
|
||||
|
||||
it 'sorts least popular merge requests' do
|
||||
get :merge_requests, params: { id: group.to_param, sort: 'downvotes_desc' }
|
||||
expect(assigns(:merge_requests)).to eq [merge_request_2, merge_request_1]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'rendering views' do
|
||||
before do
|
||||
stub_feature_flags(vue_merge_request_list: false)
|
||||
end
|
||||
|
||||
render_views
|
||||
|
||||
it 'displays MR counts in nav' do
|
||||
get :merge_requests, params: { id: group.to_param }
|
||||
|
||||
expect(response.body).to have_content('Open 2 Merged 0 Closed 0 All 2')
|
||||
expect(response.body).not_to have_content('Open Merged Closed All')
|
||||
end
|
||||
|
||||
context 'when MergeRequestsFinder raises an exception' do
|
||||
before do
|
||||
allow_next_instance_of(MergeRequestsFinder) do |instance|
|
||||
allow(instance).to receive(:count_by_state).and_raise(ActiveRecord::QueryCanceled)
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not display MR counts in nav' do
|
||||
get :merge_requests, params: { id: group.to_param }
|
||||
|
||||
expect(response.body).to have_content('Open Merged Closed All')
|
||||
expect(response.body).not_to have_content('Open 0 Merged 0 Closed 0 All 0')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an ActiveRecord::QueryCanceled is raised' do
|
||||
before do
|
||||
stub_feature_flags(vue_merge_request_list: false)
|
||||
|
||||
allow_next_instance_of(Gitlab::IssuableMetadata) do |instance|
|
||||
allow(instance).to receive(:data).and_raise(ActiveRecord::QueryCanceled)
|
||||
end
|
||||
end
|
||||
|
||||
it 'sets :search_timeout_occurred' do
|
||||
get :merge_requests, params: { id: group.to_param }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(assigns(:search_timeout_occurred)).to eq(true)
|
||||
end
|
||||
|
||||
it 'logs the exception' do
|
||||
get :merge_requests, params: { id: group.to_param }
|
||||
end
|
||||
|
||||
context 'rendering views' do
|
||||
render_views
|
||||
|
||||
it 'shows error message' do
|
||||
get :merge_requests, params: { id: group.to_param }
|
||||
|
||||
expect(response.body).to have_content('Too many results to display. Edit your search or add a filter.')
|
||||
end
|
||||
|
||||
it 'does not display MR counts in nav' do
|
||||
get :merge_requests, params: { id: group.to_param }
|
||||
|
||||
expect(response.body).to have_content('Open Merged Closed All')
|
||||
expect(response.body).not_to have_content('Open 0 Merged 0 Closed 0 All 0')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE #destroy' do
|
||||
|
|
|
|||
|
|
@ -392,77 +392,6 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :code_review
|
|||
|
||||
expect(response).to render_template('projects/merge_requests/index')
|
||||
end
|
||||
|
||||
context 'when vue_merge_request_list is disabled' do
|
||||
before do
|
||||
stub_feature_flags(vue_merge_request_list: false)
|
||||
end
|
||||
|
||||
context 'when the test is flaky', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/450217' do
|
||||
it_behaves_like "issuables list meta-data", :merge_request
|
||||
end
|
||||
|
||||
it_behaves_like 'set sort order from user preference' do
|
||||
let(:sorting_param) { 'updated_asc' }
|
||||
end
|
||||
|
||||
context 'when page param' do
|
||||
let(:last_page) { project.merge_requests.page.total_pages }
|
||||
let!(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
|
||||
|
||||
it 'redirects to last_page if page number is larger than number of pages' do
|
||||
get_merge_requests(last_page + 1)
|
||||
|
||||
expect(response).to redirect_to(project_merge_requests_path(project, page: last_page, state: controller.params[:state], scope: controller.params[:scope]))
|
||||
end
|
||||
|
||||
it 'redirects to specified page' do
|
||||
get_merge_requests(last_page)
|
||||
|
||||
expect(assigns(:merge_requests).current_page).to eq(last_page)
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'does not redirect to external sites when provided a host field' do
|
||||
external_host = "www.example.com"
|
||||
get :index,
|
||||
params: {
|
||||
namespace_id: project.namespace.to_param,
|
||||
project_id: project,
|
||||
state: 'opened',
|
||||
page: (last_page + 1).to_param,
|
||||
host: external_host
|
||||
}
|
||||
|
||||
expect(response).to redirect_to(project_merge_requests_path(project, page: last_page, state: controller.params[:state], scope: controller.params[:scope]))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering by opened state' do
|
||||
context 'with opened merge requests' do
|
||||
it 'lists those merge requests' do
|
||||
expect(merge_request).to be_persisted
|
||||
|
||||
get_merge_requests
|
||||
|
||||
expect(assigns(:merge_requests)).to include(merge_request)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with reopened merge requests' do
|
||||
before do
|
||||
merge_request.close!
|
||||
merge_request.reopen!
|
||||
end
|
||||
|
||||
it 'lists those merge requests' do
|
||||
get_merge_requests
|
||||
|
||||
expect(assigns(:merge_requests)).to include(merge_request)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT update' do
|
||||
|
|
|
|||
|
|
@ -47,24 +47,5 @@ RSpec.describe 'Groups > User sees users dropdowns in issuables list', :js, feat
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when vue_merge_request_list feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(vue_merge_request_list: false)
|
||||
end
|
||||
|
||||
%w[author assignee].each do |dropdown|
|
||||
describe "#{dropdown} dropdown" do
|
||||
it 'only includes members of the project/group' do
|
||||
visit merge_requests_group_path(group)
|
||||
|
||||
filtered_search.set("#{dropdown}:=")
|
||||
|
||||
expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).to have_content(user_in_dropdown.name)
|
||||
expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).not_to have_content(user_not_in_dropdown.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import htmlMergeRequestList from 'test_fixtures/merge_requests/merge_request_list.html';
|
||||
import htmlMergeRequestList from 'test_fixtures_static/merge_request_list.html';
|
||||
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
|
||||
import DropdownUser from '~/filtered_search/dropdown_user';
|
||||
import DropdownUtils from '~/filtered_search/dropdown_utils';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import htmlMergeRequestList from 'test_fixtures/merge_requests/merge_request_list.html';
|
||||
import htmlMergeRequestList from 'test_fixtures_static/merge_request_list.html';
|
||||
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
|
||||
import FilteredSearchSpecHelper from 'helpers/filtered_search_spec_helper';
|
||||
import DropdownUtils from '~/filtered_search/dropdown_utils';
|
||||
|
|
|
|||
|
|
@ -127,19 +127,6 @@ RSpec
|
|||
end
|
||||
end
|
||||
|
||||
it 'merge_requests/merge_request_list.html' do
|
||||
stub_feature_flags(vue_merge_request_list: false)
|
||||
|
||||
create(:merge_request, source_project: project, target_project: project)
|
||||
|
||||
get :index, params: {
|
||||
namespace_id: project.namespace.to_param,
|
||||
project_id: project
|
||||
}
|
||||
|
||||
expect(response).to be_successful
|
||||
end
|
||||
|
||||
describe GraphQL::Query, type: :request do
|
||||
include ApiHelpers
|
||||
include GraphqlHelpers
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -36,7 +36,7 @@ describe('PipelineStatusChart', () => {
|
|||
bars: [
|
||||
{ data: [], name: 'Successful' },
|
||||
{ data: [], name: 'Failed' },
|
||||
{ data: [], name: 'Other' },
|
||||
{ data: [], name: 'Other (Cancelled, Skipped)' },
|
||||
],
|
||||
groupBy: [],
|
||||
customPalette: ['#619025', '#b93d71', '#617ae2'],
|
||||
|
|
@ -61,7 +61,7 @@ describe('PipelineStatusChart', () => {
|
|||
expect(findStackedColumnChart().props('bars')).toEqual([
|
||||
{ data: [10, 11], name: 'Successful' },
|
||||
{ data: [20, 21], name: 'Failed' },
|
||||
{ data: [30, 31], name: 'Other' },
|
||||
{ data: [30, 31], name: 'Other (Cancelled, Skipped)' },
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { nextTick } from 'vue';
|
|||
import { GlDatepicker, GlFormCheckbox } from '@gitlab/ui';
|
||||
import originalOneReleaseForEditingQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release_for_editing.query.graphql.json';
|
||||
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
|
||||
import createMilestoneComboboxState from '~/milestones/stores/state';
|
||||
import { convertOneReleaseGraphQLResponse } from '~/releases/util';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import setWindowLocation from 'helpers/set_window_location_helper';
|
||||
|
|
@ -86,6 +87,25 @@ describe('Release edit/new component', () => {
|
|||
merge(
|
||||
{
|
||||
modules: {
|
||||
milestoneCombobox: {
|
||||
namespaced: true,
|
||||
actions: {
|
||||
setProjectId: jest.fn(),
|
||||
setGroupId: jest.fn(),
|
||||
setGroupMilestonesAvailable: jest.fn(),
|
||||
setSelectedMilestones: jest.fn(),
|
||||
clearSelectedMilestones: jest.fn(),
|
||||
toggleMilestones: jest.fn(),
|
||||
search: jest.fn(),
|
||||
fetchMilestones: jest.fn(),
|
||||
fetchProjectMilestones: jest.fn(),
|
||||
fetchGroupMilestones: jest.fn(),
|
||||
searchProjectMilestones: jest.fn(),
|
||||
searchGroupMilestones: jest.fn(),
|
||||
},
|
||||
state: createMilestoneComboboxState(),
|
||||
getters: { isLoading: jest.fn() },
|
||||
},
|
||||
editNew: {
|
||||
namespaced: true,
|
||||
actions,
|
||||
|
|
@ -239,6 +259,7 @@ describe('Release edit/new component', () => {
|
|||
store: {
|
||||
modules: {
|
||||
editNew: {
|
||||
namespaced: true,
|
||||
state: { isExistingRelease: false },
|
||||
},
|
||||
},
|
||||
|
|
@ -280,6 +301,7 @@ describe('Release edit/new component', () => {
|
|||
store: {
|
||||
modules: {
|
||||
editNew: {
|
||||
namespaced: true,
|
||||
getters: {
|
||||
isValid: () => true,
|
||||
},
|
||||
|
|
@ -300,6 +322,7 @@ describe('Release edit/new component', () => {
|
|||
store: {
|
||||
modules: {
|
||||
editNew: {
|
||||
namespaced: true,
|
||||
getters: {
|
||||
isValid: () => false,
|
||||
},
|
||||
|
|
@ -326,6 +349,7 @@ describe('Release edit/new component', () => {
|
|||
store: {
|
||||
modules: {
|
||||
editNew: {
|
||||
namespaced: true,
|
||||
state: {
|
||||
isFetchingTagNotes: true,
|
||||
},
|
||||
|
|
@ -355,6 +379,7 @@ describe('Release edit/new component', () => {
|
|||
store: {
|
||||
modules: {
|
||||
editNew: {
|
||||
namespaced: true,
|
||||
state: {
|
||||
isExistingRelease: false,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ require 'spec_helper'
|
|||
RSpec.describe 'validate database config', feature_category: :cell do
|
||||
include StubENV
|
||||
|
||||
let(:dev_message) do
|
||||
"\nMake sure your development environment is up to date.\nFor example, on GDK, run: gdk update\n"
|
||||
end
|
||||
|
||||
let(:rails_configuration) { Rails::Application::Configuration.new(Rails.root) }
|
||||
let(:valid_topology_service_client_config) do
|
||||
{
|
||||
|
|
@ -71,22 +75,32 @@ RSpec.describe 'validate database config', feature_category: :cell do
|
|||
end
|
||||
|
||||
it 'raises an exception' do
|
||||
expect { validate_config }.to raise_error("Cell ID is set but Cell is not enabled")
|
||||
expect { validate_config }.to raise_error("Cell ID is set but Cell is not enabled.#{dev_message}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when configuration is wrong' do
|
||||
context 'when configuration is invalid' do
|
||||
context 'when cell is enabled by cell id is not set' do
|
||||
before do
|
||||
stub_config(cell: { enabled: true, id: nil, topology_service_client: valid_topology_service_client_config })
|
||||
end
|
||||
|
||||
it 'raises exception about missing cell id' do
|
||||
expect { validate_config }.to raise_error("Cell ID is not set to a valid positive integer")
|
||||
expect { validate_config }.to raise_error("Cell ID is not set to a valid positive integer.#{dev_message}")
|
||||
end
|
||||
|
||||
it_behaves_like 'with SKIP_CELL_CONFIG_VALIDATION=true'
|
||||
|
||||
context 'when not dev environment' do
|
||||
before do
|
||||
stub_rails_env('production')
|
||||
end
|
||||
|
||||
it 'raises exception about missing cell id' do
|
||||
expect { validate_config }.to raise_error("Cell ID is not set to a valid positive integer.")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when cell is enabled by cell id is not valid' do
|
||||
|
|
@ -95,7 +109,7 @@ RSpec.describe 'validate database config', feature_category: :cell do
|
|||
end
|
||||
|
||||
it 'raises exception about missing cell id' do
|
||||
expect { validate_config }.to raise_error("Cell ID is not set to a valid positive integer")
|
||||
expect { validate_config }.to raise_error("Cell ID is not set to a valid positive integer.#{dev_message}")
|
||||
end
|
||||
|
||||
it_behaves_like 'with SKIP_CELL_CONFIG_VALIDATION=true'
|
||||
|
|
@ -107,7 +121,7 @@ RSpec.describe 'validate database config', feature_category: :cell do
|
|||
end
|
||||
|
||||
it 'raises exception about missing topology service client config' do
|
||||
expect { validate_config }.to raise_error("Topology Service setting 'address' is not set")
|
||||
expect { validate_config }.to raise_error("Topology Service setting 'address' is not set.#{dev_message}")
|
||||
end
|
||||
|
||||
it_behaves_like 'with SKIP_CELL_CONFIG_VALIDATION=true'
|
||||
|
|
|
|||
|
|
@ -770,6 +770,20 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana
|
|||
allow(event_definition).to receive(:extra_tracking_classes).and_return([custom_tracking_class])
|
||||
end
|
||||
|
||||
context 'when event is not defined' do
|
||||
let(:event_name) { 'an_event_that_does_not_exist' }
|
||||
|
||||
before do
|
||||
allow(Gitlab::Tracking::EventDefinition).to receive(:find).with(event_name).and_return(nil)
|
||||
end
|
||||
|
||||
it 'does not call custom classes' do
|
||||
expect(custom_tracking_class).not_to receive(:track_event)
|
||||
|
||||
described_class.track_event(event_name, user: user, project: project)
|
||||
end
|
||||
end
|
||||
|
||||
it 'calls the custom classes' do
|
||||
expect(custom_tracking_class).to receive(:track_event).with(event_name, **event_kwargs)
|
||||
|
||||
|
|
|
|||
|
|
@ -2624,7 +2624,7 @@ RSpec.describe User, feature_category: :user_profile do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#forget_me!' do
|
||||
describe '#invalidate_all_remember_tokens!' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
context 'when remember me application setting is disabled' do
|
||||
|
|
@ -2638,7 +2638,7 @@ RSpec.describe User, feature_category: :user_profile do
|
|||
expect(user.remember_created_at).not_to be_nil
|
||||
|
||||
stub_application_setting(remember_me_enabled: false)
|
||||
user.forget_me!
|
||||
user.invalidate_all_remember_tokens!
|
||||
|
||||
expect(user.remember_created_at).to be_nil
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe API::PackageFiles, feature_category: :package_registry do
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project, :public) }
|
||||
let_it_be(:project) { create(:project, :public) }
|
||||
let(:package) { create(:maven_package, project: project) }
|
||||
|
||||
describe 'GET /projects/:id/packages/:package_id/package_files' do
|
||||
|
|
@ -274,5 +274,112 @@ RSpec.describe API::PackageFiles, feature_category: :package_registry do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with package protection rule for different roles and package_name_patterns', :enable_admin_mode do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
let_it_be(:pat_project_maintainer) do
|
||||
create(:personal_access_token, user: create(:user, maintainer_of: [project]))
|
||||
end
|
||||
|
||||
let_it_be(:pat_project_owner) { create(:personal_access_token, user: create(:user, owner_of: [project])) }
|
||||
let_it_be(:pat_instance_admin) { create(:personal_access_token, :admin_mode, user: create(:admin)) }
|
||||
let_it_be(:headers_pat_project_maintainer) do
|
||||
{ Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => pat_project_maintainer.token }
|
||||
end
|
||||
|
||||
let_it_be(:headers_pat_project_owner) do
|
||||
{ Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => pat_project_owner.token }
|
||||
end
|
||||
|
||||
let_it_be(:headers_pat_instance_admin) do
|
||||
{ Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => pat_instance_admin.token }
|
||||
end
|
||||
|
||||
let_it_be(:job_from_project_maintainer) do
|
||||
create(:ci_build, :running, user: pat_project_maintainer.user, project: project)
|
||||
end
|
||||
|
||||
let_it_be(:job_from_project_owner) { create(:ci_build, :running, user: pat_project_owner.user, project: project) }
|
||||
let(:headers_job_token_from_maintainer) do
|
||||
{ Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER => job_from_project_maintainer.token }
|
||||
end
|
||||
|
||||
let(:headers_job_token_from_owner) do
|
||||
{ Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER => job_from_project_owner.token }
|
||||
end
|
||||
|
||||
let(:package_protection_rule) { create(:package_protection_rule, project: project) }
|
||||
|
||||
let(:package_name) { package.name }
|
||||
let(:package_name_no_match) { "#{package_name}_no_match" }
|
||||
|
||||
subject do
|
||||
delete api(url), headers: headers
|
||||
response
|
||||
end
|
||||
|
||||
shared_examples 'deleting package protected' do
|
||||
it_behaves_like 'returning response status', :forbidden
|
||||
it 'responds with correct error message' do
|
||||
subject
|
||||
|
||||
expect(json_response).to include('message' => "403 Forbidden - Package is deletion protected.")
|
||||
end
|
||||
|
||||
it { expect { subject }.not_to change { ::Packages::Package.pending_destruction.count } }
|
||||
|
||||
context 'when feature flag :packages_protected_packages_delete disabled' do
|
||||
before do
|
||||
stub_feature_flags(packages_protected_packages_delete: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'deleting package'
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'deleting package' do
|
||||
it_behaves_like 'returning response status', :no_content
|
||||
it { expect { subject }.to change { package.package_files.pending_destruction.count }.by(1) }
|
||||
end
|
||||
|
||||
where(:package_name_pattern, :minimum_access_level_for_delete, :headers, :shared_examples_name) do
|
||||
ref(:package_name) | :owner | ref(:headers_job_token_from_maintainer) | 'deleting package protected'
|
||||
ref(:package_name) | :owner | ref(:headers_job_token_from_owner) | 'deleting package'
|
||||
ref(:package_name) | :owner | ref(:headers_pat_project_maintainer) | 'deleting package protected'
|
||||
ref(:package_name) | :owner | ref(:headers_pat_project_owner) | 'deleting package'
|
||||
ref(:package_name) | :owner | ref(:headers_pat_instance_admin) | 'deleting package'
|
||||
|
||||
ref(:package_name) | :admin | ref(:headers_pat_project_maintainer) | 'deleting package protected'
|
||||
ref(:package_name) | :admin | ref(:headers_pat_project_owner) | 'deleting package protected'
|
||||
ref(:package_name) | :admin | ref(:headers_job_token_from_owner) | 'deleting package protected'
|
||||
ref(:package_name) | :admin | ref(:headers_pat_instance_admin) | 'deleting package'
|
||||
|
||||
ref(:package_name_no_match) | :owner | ref(:headers_pat_project_owner) | 'deleting package'
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
package_protection_rule.update!(
|
||||
package_name_pattern: package_name_pattern,
|
||||
package_type: package.package_type,
|
||||
minimum_access_level_for_delete: minimum_access_level_for_delete
|
||||
)
|
||||
end
|
||||
|
||||
it_behaves_like params[:shared_examples_name]
|
||||
end
|
||||
|
||||
context 'for package with unsupported package type for package protection rule' do
|
||||
let_it_be(:nuget_package) { create(:nuget_package, project: project) }
|
||||
|
||||
let(:package) { nuget_package }
|
||||
let(:package_file_id) { nuget_package.package_files.first.id }
|
||||
|
||||
let(:headers) { headers_pat_project_maintainer }
|
||||
|
||||
it_behaves_like 'deleting package'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -180,6 +180,37 @@ RSpec.describe API::PersonalAccessTokens, :aggregate_failures, feature_category:
|
|||
end
|
||||
end
|
||||
|
||||
context 'filter with expires parameter' do
|
||||
let_it_be(:token1) { create(:personal_access_token, expires_at: Date.new(2022, 01, 01)) }
|
||||
|
||||
context 'test expires_before' do
|
||||
where(:expires_at, :status, :result_count, :result) do
|
||||
'2022-01-02' | :ok | 1 | lazy { [token1.id] }
|
||||
'2022-01-01' | :ok | 0 | lazy { [] }
|
||||
'2022-01-01T12:30:24' | :ok | 0 | lazy { [] }
|
||||
'asdf' | :bad_request | 1 | { "error" => "expires_before is invalid" }
|
||||
end
|
||||
|
||||
with_them do
|
||||
it_behaves_like 'response as expected', expires_before: params[:expires_at]
|
||||
end
|
||||
end
|
||||
|
||||
context 'test expires_after' do
|
||||
where(:expires_at, :status, :result_count, :result) do
|
||||
'2022-01-03' | :ok | 1 | lazy { [current_users_token.id] }
|
||||
'2022-01-01' | :ok | 2 | lazy { [token1.id, current_users_token.id] }
|
||||
'2022-01-01T12:30:26' | :ok | 2 | lazy { [token1.id, current_users_token.id] }
|
||||
(DateTime.now + 1).to_s | :ok | 1 | lazy { [current_users_token.id] }
|
||||
'asdf' | :bad_request | 1 | { "error" => "expires_after is invalid" }
|
||||
end
|
||||
|
||||
with_them do
|
||||
it_behaves_like 'response as expected', expires_after: params[:expires_at]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'filter with search parameter' do
|
||||
let_it_be(:token1) { create(:personal_access_token, name: 'test_1') }
|
||||
let_it_be(:token2) { create(:personal_access_token, name: 'test_2') }
|
||||
|
|
|
|||
|
|
@ -735,6 +735,94 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do
|
|||
expect(response).to have_gitlab_http_status(:no_content)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with package protection rule for different roles and package_name_patterns', :enable_admin_mode do
|
||||
let_it_be(:pat_project_maintainer) { create(:personal_access_token, user: create(:user, maintainer_of: [project])) }
|
||||
let_it_be(:pat_project_owner) { create(:personal_access_token, user: create(:user, owner_of: [project])) }
|
||||
let_it_be(:pat_instance_admin) { create(:personal_access_token, :admin_mode, user: create(:admin)) }
|
||||
let_it_be(:headers_pat_project_maintainer) { { Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => pat_project_maintainer.token } }
|
||||
let_it_be(:headers_pat_project_owner) { { Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => pat_project_owner.token } }
|
||||
let_it_be(:headers_pat_instance_admin) { { Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => pat_instance_admin.token } }
|
||||
let_it_be(:job_from_project_maintainer) { create(:ci_build, :running, user: pat_project_maintainer.user, project: project) }
|
||||
let_it_be(:job_from_project_owner) { create(:ci_build, :running, user: pat_project_owner.user, project: project) }
|
||||
|
||||
let_it_be(:headers_job_token_from_maintainer) { { Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER => job_from_project_maintainer.token } }
|
||||
let_it_be(:headers_job_token_from_owner) { { Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER => job_from_project_owner.token } }
|
||||
|
||||
let(:package_protection_rule) { create(:package_protection_rule, project: project) }
|
||||
|
||||
let(:package_name) { package1.name }
|
||||
let(:package_name_no_match) { "#{package_name}_no_match" }
|
||||
|
||||
subject do
|
||||
delete api(package_url), headers: headers
|
||||
response
|
||||
end
|
||||
|
||||
shared_examples 'deleting package protected' do
|
||||
it_behaves_like 'returning response status', :forbidden
|
||||
it do
|
||||
subject
|
||||
|
||||
expect(json_response).to include('message' => "403 Forbidden - Package is deletion protected.")
|
||||
end
|
||||
|
||||
it { expect { subject }.not_to change { ::Packages::Package.pending_destruction.count } }
|
||||
|
||||
context 'when feature flag :packages_protected_packages_delete disabled' do
|
||||
before do
|
||||
stub_feature_flags(packages_protected_packages_delete: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'deleting package'
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'deleting package' do
|
||||
it_behaves_like 'returning response status', :no_content
|
||||
it { expect { subject }.to change { ::Packages::Package.pending_destruction.count }.by(1) }
|
||||
end
|
||||
|
||||
where(:package_name_pattern, :minimum_access_level_for_delete, :headers, :shared_examples_name) do
|
||||
ref(:package_name) | :owner | ref(:headers_job_token_from_maintainer) | 'deleting package protected'
|
||||
ref(:package_name) | :owner | ref(:headers_job_token_from_owner) | 'deleting package'
|
||||
ref(:package_name) | :owner | ref(:headers_pat_project_maintainer) | 'deleting package protected'
|
||||
ref(:package_name) | :owner | ref(:headers_pat_project_owner) | 'deleting package'
|
||||
ref(:package_name) | :owner | ref(:headers_pat_instance_admin) | 'deleting package'
|
||||
|
||||
ref(:package_name) | :admin | ref(:headers_job_token_from_owner) | 'deleting package protected'
|
||||
ref(:package_name) | :admin | ref(:headers_pat_project_maintainer) | 'deleting package protected'
|
||||
ref(:package_name) | :admin | ref(:headers_pat_project_owner) | 'deleting package protected'
|
||||
ref(:package_name) | :admin | ref(:headers_pat_instance_admin) | 'deleting package'
|
||||
|
||||
ref(:package_name_no_match) | :owner | ref(:headers_pat_project_owner) | 'deleting package'
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
package_protection_rule.update!(
|
||||
package_name_pattern: package_name_pattern,
|
||||
package_type: package1.package_type,
|
||||
minimum_access_level_for_delete: minimum_access_level_for_delete
|
||||
)
|
||||
end
|
||||
|
||||
it_behaves_like params[:shared_examples_name]
|
||||
end
|
||||
|
||||
context 'for package with unsupported package type for package protection rule' do
|
||||
let_it_be(:golang_package) { create(:golang_package, project: project, name: "#{project.root_namespace.path}/golang.org/x/pkg") }
|
||||
|
||||
let(:headers) { headers_pat_project_maintainer }
|
||||
|
||||
subject do
|
||||
delete api("/projects/#{project.id}/packages/#{golang_package.id}"), headers: headers
|
||||
response
|
||||
end
|
||||
|
||||
it_behaves_like 'deleting package'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a maven package' do
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ require "spec_helper"
|
|||
|
||||
RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:user_non_priviledged) { create(:user) }
|
||||
let_it_be(:user_non_privileged) { create(:user) }
|
||||
|
||||
shared_examples 'resource access token API' do |source_type|
|
||||
context "GET #{source_type}s/:id/access_tokens" do
|
||||
|
|
@ -13,7 +13,11 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
|
|||
context "when the user has valid permissions" do
|
||||
let_it_be(:project_bot) { create(:user, :project_bot, bot_namespace: namespace) }
|
||||
let_it_be(:active_access_tokens) { create_list(:personal_access_token, 5, user: project_bot) }
|
||||
let_it_be(:expired_token) { create(:personal_access_token, :expired, user: project_bot) }
|
||||
let_it_be(:expired_token) do
|
||||
create(:personal_access_token, :expired, expires_at: 2.days.ago, last_used_at: 2.days.ago, name: 'a_test_1',
|
||||
user: project_bot)
|
||||
end
|
||||
|
||||
let_it_be(:revoked_token) { create(:personal_access_token, :revoked, user: project_bot) }
|
||||
let_it_be(:inactive_access_tokens) { [expired_token, revoked_token] }
|
||||
let_it_be(:all_access_tokens) { active_access_tokens + inactive_access_tokens }
|
||||
|
|
@ -122,39 +126,128 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when state param is set to inactive' do
|
||||
let(:params) { { state: 'inactive' } }
|
||||
|
||||
it 'returns only inactive tokens' do
|
||||
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: params
|
||||
context 'when filtering by revoked' do
|
||||
it 'returns non-revoked tokens when revoked is false' do
|
||||
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { revoked: false }
|
||||
|
||||
token_ids = json_response.map { |token| token['id'] }
|
||||
expect(token_ids).to match_array(all_access_tokens.pluck(:id).reject { |n| n == revoked_token.id })
|
||||
end
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to include_pagination_headers
|
||||
expect(response).to match_response_schema('public_api/v4/resource_access_tokens')
|
||||
expect(token_ids).to match_array(inactive_access_tokens.pluck(:id))
|
||||
it 'returns revoked tokens when revoked is true' do
|
||||
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { revoked: true }
|
||||
|
||||
token_ids = json_response.map { |token| token['id'] }
|
||||
expect(token_ids).to match_array([revoked_token.id])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when state param is set to active' do
|
||||
let(:params) { { state: 'active' } }
|
||||
context 'when filtering by state' do
|
||||
context 'when state param is set to inactive' do
|
||||
let(:params) { { state: 'inactive' } }
|
||||
|
||||
it 'returns only active tokens' do
|
||||
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: params
|
||||
it 'returns only inactive tokens' do
|
||||
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: params
|
||||
|
||||
token_ids = json_response.map { |token| token['id'] }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to include_pagination_headers
|
||||
expect(response).to match_response_schema('public_api/v4/resource_access_tokens')
|
||||
expect(token_ids).to match_array(inactive_access_tokens.pluck(:id))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when state param is set to active' do
|
||||
let(:params) { { state: 'active' } }
|
||||
|
||||
it 'returns only active tokens' do
|
||||
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: params
|
||||
|
||||
token_ids = json_response.map { |token| token['id'] }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to include_pagination_headers
|
||||
expect(response).to match_response_schema('public_api/v4/resource_access_tokens')
|
||||
expect(token_ids).to match_array(active_access_tokens.pluck(:id))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering by created dates' do
|
||||
it 'returns tokens created before specified date' do
|
||||
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { created_before: 1.day.ago }
|
||||
|
||||
expect(json_response).to be_empty
|
||||
end
|
||||
|
||||
it 'returns tokens created after specified date' do
|
||||
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { created_after: 1.day.ago }
|
||||
|
||||
token_ids = json_response.map { |token| token['id'] }
|
||||
expect(token_ids).to match_array(all_access_tokens.pluck(:id))
|
||||
end
|
||||
end
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to include_pagination_headers
|
||||
expect(response).to match_response_schema('public_api/v4/resource_access_tokens')
|
||||
expect(token_ids).to match_array(active_access_tokens.pluck(:id))
|
||||
context 'when filtering by last used dates' do
|
||||
it 'returns tokens last used before specified date' do
|
||||
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { last_used_before: 1.day.ago }
|
||||
|
||||
token_ids = json_response.map { |token| token['id'] }
|
||||
expect(token_ids).to match_array([expired_token.id])
|
||||
end
|
||||
|
||||
it 'returns tokens last used after specified date' do
|
||||
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { last_used_after: 1.day.ago }
|
||||
|
||||
expect(json_response).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering by expiration dates' do
|
||||
it 'returns tokens that expire before specified date' do
|
||||
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { expires_before: 1.day.ago }
|
||||
|
||||
token_ids = json_response.map { |token| token['id'] }
|
||||
expect(token_ids).to match_array([expired_token.id])
|
||||
end
|
||||
|
||||
it 'returns tokens that expire after specified date' do
|
||||
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { expires_after: 1.day.ago }
|
||||
|
||||
token_ids = json_response.map { |token| token['id'] }
|
||||
expect(token_ids).to match_array(all_access_tokens.pluck(:id).reject { |n| n == expired_token.id })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when searching by name' do
|
||||
it 'returns tokens matching the search term' do
|
||||
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { search: 'a_test_1' }
|
||||
|
||||
token_ids = json_response.map { |token| token['id'] }
|
||||
expect(token_ids).to match_array([expired_token.id])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sorting' do
|
||||
it 'sorts tokens by last_used_desc when specified' do
|
||||
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { sort: 'name_desc' }
|
||||
|
||||
token_ids = json_response.map { |token| token['id'] }
|
||||
expect(token_ids.last).to eq(expired_token.id)
|
||||
end
|
||||
|
||||
it 'sorts tokens by last_used_asc when specified' do
|
||||
get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { sort: 'name_asc' }
|
||||
|
||||
token_ids = json_response.map { |token| token['id'] }
|
||||
expect(token_ids.first).to eq(expired_token.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when the user does not have valid permissions" do
|
||||
let_it_be(:user) { user_non_priviledged }
|
||||
let_it_be(:user) { user_non_privileged }
|
||||
let_it_be(:project_bot) { create(:user, :project_bot, bot_namespace: namespace) }
|
||||
let_it_be(:access_tokens) { create_list(:personal_access_token, 3, user: project_bot) }
|
||||
let_it_be(:resource_id) { resource.id }
|
||||
|
|
@ -266,7 +359,7 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
|
|||
end
|
||||
|
||||
context "when the user does not have valid permissions" do
|
||||
let_it_be(:user) { user_non_priviledged }
|
||||
let_it_be(:user) { user_non_privileged }
|
||||
|
||||
it "returns 401" do
|
||||
get_token
|
||||
|
|
@ -340,7 +433,7 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
|
|||
end
|
||||
|
||||
context "when the user does not have valid permissions" do
|
||||
let_it_be(:user) { user_non_priviledged }
|
||||
let_it_be(:user) { user_non_privileged }
|
||||
|
||||
it "does not delete the token, and returns 400", :aggregate_failures do
|
||||
delete_token
|
||||
|
|
@ -483,7 +576,7 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
|
|||
let_it_be(:resource_id) { resource.id }
|
||||
|
||||
context "when the user role is too low" do
|
||||
let_it_be(:user) { user_non_priviledged }
|
||||
let_it_be(:user) { user_non_privileged }
|
||||
|
||||
it "does not create the token, and returns the permission error" do
|
||||
create_token
|
||||
|
|
@ -687,7 +780,7 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
|
|||
before_all do
|
||||
resource.add_maintainer(user)
|
||||
other_resource.add_maintainer(user)
|
||||
resource.add_developer(user_non_priviledged)
|
||||
resource.add_developer(user_non_privileged)
|
||||
end
|
||||
|
||||
it_behaves_like 'resource access token API', 'project'
|
||||
|
|
@ -703,7 +796,7 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
|
|||
before_all do
|
||||
resource.add_owner(user)
|
||||
other_resource.add_owner(user)
|
||||
resource.add_maintainer(user_non_priviledged)
|
||||
resource.add_maintainer(user_non_privileged)
|
||||
end
|
||||
|
||||
it_behaves_like 'resource access token API', 'group'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Packages::Protection::CheckDeleteRuleExistenceService, feature_category: :package_registry do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:unauthorized_user) { create(:user) }
|
||||
let_it_be(:project_developer) { create(:user, developer_of: project) }
|
||||
let_it_be(:project_maintainer) { create(:user, maintainer_of: project) }
|
||||
let_it_be(:project_owner) { project.owner }
|
||||
let_it_be(:instance_admin) { create(:admin) }
|
||||
|
||||
let_it_be(:container_protection_rule_npm) do
|
||||
create(:package_protection_rule,
|
||||
project: project,
|
||||
package_type: :npm,
|
||||
package_name_pattern: "@#{project.full_path}*",
|
||||
minimum_access_level_for_delete: :owner)
|
||||
end
|
||||
|
||||
let_it_be(:container_protection_rule_pypi) do
|
||||
create(:package_protection_rule,
|
||||
project: project,
|
||||
package_type: :pypi,
|
||||
package_name_pattern: "#{project.full_path}*",
|
||||
minimum_access_level_for_delete: :admin)
|
||||
end
|
||||
|
||||
let(:params) { { package_name: package_name, package_type: package_type } }
|
||||
let(:service) { described_class.new(project: project, current_user: current_user, params: params) }
|
||||
|
||||
subject(:service_response) { service.execute }
|
||||
|
||||
shared_examples 'a service response for protection rule exists' do
|
||||
it_behaves_like 'returning a success service response'
|
||||
it { is_expected.to have_attributes(payload: { protection_rule_exists?: true }) }
|
||||
end
|
||||
|
||||
shared_examples 'a service response for protection rule does not exist' do
|
||||
it_behaves_like 'returning a success service response'
|
||||
it { is_expected.to have_attributes(payload: { protection_rule_exists?: false }) }
|
||||
end
|
||||
|
||||
shared_examples 'an error service response for unauthorized actor' do
|
||||
it_behaves_like 'returning an error service response', message: 'Unauthorized'
|
||||
it { is_expected.to have_attributes reason: :unauthorized }
|
||||
end
|
||||
|
||||
shared_examples 'an error service response for invalid package type' do
|
||||
it_behaves_like 'returning an error service response', message: 'Invalid package type'
|
||||
it { is_expected.to have_attributes reason: :invalid_package_type }
|
||||
end
|
||||
|
||||
describe '#execute', :enable_admin_mode do
|
||||
# rubocop:disable Layout/LineLength -- Avoid formatting in favor of one-line table syntax
|
||||
where(:package_name, :package_type, :current_user, :expected_shared_example) do
|
||||
lazy { "@#{project.full_path}" } | :npm | ref(:project_developer) | 'an error service response for unauthorized actor'
|
||||
lazy { "@#{project.full_path}" } | :npm | ref(:project_maintainer) | 'a service response for protection rule exists'
|
||||
lazy { "@#{project.full_path}" } | :npm | ref(:project_owner) | 'a service response for protection rule does not exist'
|
||||
lazy { "@#{project.full_path}" } | :npm | ref(:instance_admin) | 'a service response for protection rule does not exist'
|
||||
lazy { "@other-scope/#{project.full_path}" } | :npm | ref(:project_maintainer) | 'a service response for protection rule does not exist'
|
||||
lazy { "@other-scope/#{project.full_path}" } | :npm | ref(:project_owner) | 'a service response for protection rule does not exist'
|
||||
lazy { project.full_path } | :pypi | ref(:project_maintainer) | 'a service response for protection rule exists'
|
||||
lazy { project.full_path } | :pypi | ref(:project_owner) | 'a service response for protection rule exists'
|
||||
lazy { project.full_path } | :pypi | ref(:instance_admin) | 'a service response for protection rule does not exist'
|
||||
|
||||
# Edge cases
|
||||
lazy { "@#{project.full_path}" } | :npm | ref(:unauthorized_user) | 'an error service response for unauthorized actor'
|
||||
lazy { "@#{project.full_path}" } | :npm | nil | 'an error service response for unauthorized actor'
|
||||
lazy { "@#{project.full_path}" } | :no_type | nil | 'an error service response for invalid package type'
|
||||
lazy { "@#{project.full_path}" } | nil | ref(:project_owner) | 'an error service response for invalid package type'
|
||||
nil | :npm | ref(:project_owner) | 'a service response for protection rule does not exist'
|
||||
nil | nil | ref(:project_owner) | 'an error service response for invalid package type'
|
||||
end
|
||||
# rubocop:enable Layout/LineLength
|
||||
|
||||
with_them do
|
||||
it_behaves_like params[:expected_shared_example]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -43,6 +43,20 @@ RSpec.shared_examples 'an access token GET API with access token params' do
|
|||
)
|
||||
end
|
||||
|
||||
context 'when filtering by revoked' do
|
||||
it 'returns not-revoked tokens when revoked is false' do
|
||||
get api_request, params: { revoked: false }
|
||||
|
||||
expect_paginated_array_response_contain_exactly(*all_token_ids.excluding(revoked_token1.id, revoked_token2.id))
|
||||
end
|
||||
|
||||
it 'returns revoked tokens when revoked is true' do
|
||||
get api_request, params: { revoked: true }
|
||||
|
||||
expect_paginated_array_response_contain_exactly(revoked_token1.id, revoked_token2.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering by state' do
|
||||
it 'returns active tokens when state is active' do
|
||||
get api_request, params: { state: 'active' }
|
||||
|
|
@ -68,20 +82,6 @@ RSpec.shared_examples 'an access token GET API with access token params' do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when filtering by revoked' do
|
||||
it 'returns not-revoked tokens when revoked is false' do
|
||||
get api_request, params: { revoked: false }
|
||||
|
||||
expect_paginated_array_response_contain_exactly(*all_token_ids.excluding(revoked_token1.id, revoked_token2.id))
|
||||
end
|
||||
|
||||
it 'returns revoked tokens when revoked is true' do
|
||||
get api_request, params: { revoked: true }
|
||||
|
||||
expect_paginated_array_response_contain_exactly(revoked_token1.id, revoked_token2.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering by created dates' do
|
||||
it 'returns tokens created before specified date' do
|
||||
get api_request, params: { created_before: 1.day.ago }
|
||||
|
|
@ -110,6 +110,20 @@ RSpec.shared_examples 'an access token GET API with access token params' do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when filtering by expiration dates' do
|
||||
it 'returns tokens that expire before specified date' do
|
||||
get api_request, params: { expires_before: 1.year.ago + 1.day }
|
||||
|
||||
expect_paginated_array_response_contain_exactly(expired_token1.id, expired_token2.id)
|
||||
end
|
||||
|
||||
it 'returns tokens that expire after specified date' do
|
||||
get api_request, params: { expires_after: 1.year.ago, expires_before: 1.week.ago }
|
||||
|
||||
expect_paginated_array_response_contain_exactly(expired_token1.id, expired_token2.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when searching by name' do
|
||||
it 'returns tokens matching the search term' do
|
||||
get api_request, params: { search: 'test' }
|
||||
|
|
|
|||
|
|
@ -1441,10 +1441,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.123.0.tgz#1fa3b1a709755ff7c8ef67e18c0442101655ebf0"
|
||||
integrity sha512-yjVn+utOTIKk8d9JlvGo6EgJ4TQ+CKpe3RddflAqtsQqQuL/2MlVdtaUePybxYzWIaumFuh5LouQ6BrWyw1niQ==
|
||||
|
||||
"@gitlab/ui@110.0.0":
|
||||
version "110.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-110.0.0.tgz#3a1fd77513063d338b8a4676453e53321f58230b"
|
||||
integrity sha512-AYiPQTQW3Hh+3j0eZr1WvuzDVNHEssxgrO66jbQO79WmSr88IXO13kICAbs0T1bvmS2vZx8VyP/TQ0ow3UJMHQ==
|
||||
"@gitlab/ui@110.1.0":
|
||||
version "110.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-110.1.0.tgz#5a38aafb92d67b589318c39f72b5ba622fa89b83"
|
||||
integrity sha512-tCezdqWgSNKuksfvVfm8TWBSIbkuK0jhCoffFFKl3HzBf9FWnCqS5+XEHLU3nPttZBTi5T761BTNqqHx8SZUAg==
|
||||
dependencies:
|
||||
"@floating-ui/dom" "1.4.3"
|
||||
echarts "^5.3.2"
|
||||
|
|
|
|||
Loading…
Reference in New Issue