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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/frontend.gitlab-ci.yml8
-rw-r--r--app/assets/javascripts/import_entities/components/group_dropdown.vue4
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue4
-rw-r--r--app/assets/javascripts/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql18
-rw-r--r--app/assets/javascripts/issues/index.js4
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/constants.js2
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index.vue13
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue13
-rw-r--r--app/assets/javascripts/notes/index.js5
-rw-r--r--app/assets/javascripts/pages/admin/jobs/components/constants.js4
-rw-r--r--app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue57
-rw-r--r--app/assets/javascripts/pages/projects/ml/experiments/index/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/ml/experiments/show/index.js13
-rw-r--r--app/assets/javascripts/projects/new/components/app.vue24
-rw-r--r--app/assets/javascripts/projects/new/index.js2
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/router.js19
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue24
-rw-r--r--app/controllers/import/base_controller.rb2
-rw-r--r--app/controllers/import/bitbucket_controller.rb4
-rw-r--r--app/controllers/import/gitea_controller.rb2
-rw-r--r--app/controllers/import/github_controller.rb2
-rw-r--r--app/controllers/import/gitlab_projects_controller.rb2
-rw-r--r--app/controllers/import/manifest_controller.rb4
-rw-r--r--app/controllers/projects/imports_controller.rb2
-rw-r--r--app/finders/groups/accepting_project_imports_finder.rb31
-rw-r--r--app/finders/groups/user_groups_finder.rb6
-rw-r--r--app/graphql/types/ci/job_type.rb15
-rw-r--r--app/graphql/types/permission_types/group_enum.rb3
-rw-r--r--app/models/packages/npm/metadatum.rb6
-rw-r--r--app/models/packages/package.rb1
-rw-r--r--app/models/project.rb10
-rw-r--r--app/policies/namespaces/user_namespace_policy.rb5
-rw-r--r--app/presenters/ci/build_presenter.rb4
-rw-r--r--app/services/groups/transfer_service.rb9
-rw-r--r--app/services/import/base_service.rb2
-rw-r--r--app/services/import/bitbucket_server_service.rb2
-rw-r--r--app/services/import/fogbugz_service.rb4
-rw-r--r--app/services/import/github_service.rb2
-rw-r--r--app/services/packages/npm/create_package_service.rb41
-rw-r--r--app/services/projects/create_service.rb8
-rw-r--r--app/services/projects/transfer_service.rb4
-rw-r--r--app/views/admin/abuse_reports/_abuse_report.html.haml2
-rw-r--r--app/views/groups/settings/access_tokens/index.html.haml6
-rw-r--r--app/views/groups/settings/ci_cd/show.html.haml2
-rw-r--r--app/views/projects/ml/experiments/index.html.haml3
-rw-r--r--app/views/projects/ml/experiments/show.html.haml3
-rw-r--r--app/views/projects/new.html.haml3
-rw-r--r--config/feature_flags/development/exit_registration_verification.yml8
-rw-r--r--config/routes.rb5
-rw-r--r--db/docs/namespace_statistics.yml5
-rw-r--r--doc/api/graphql/reference/index.md2
-rw-r--r--locale/gitlab.pot15
-rw-r--r--qa/qa/support/page/logging.rb48
-rw-r--r--qa/spec/page/logging_spec.rb16
-rw-r--r--scripts/gitlab_component_helpers.sh26
-rw-r--r--spec/controllers/import/bitbucket_controller_spec.rb13
-rw-r--r--spec/controllers/import/bitbucket_server_controller_spec.rb2
-rw-r--r--spec/controllers/import/fogbugz_controller_spec.rb2
-rw-r--r--spec/controllers/import/gitea_controller_spec.rb2
-rw-r--r--spec/controllers/import/gitlab_controller_spec.rb2
-rw-r--r--spec/controllers/import/manifest_controller_spec.rb4
-rw-r--r--spec/controllers/projects/imports_controller_spec.rb14
-rw-r--r--spec/finders/groups/accepting_project_imports_finder_spec.rb105
-rw-r--r--spec/finders/groups/user_groups_finder_spec.rb20
-rw-r--r--spec/frontend/import_entities/components/group_dropdown_spec.js4
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js4
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js4
-rw-r--r--spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js106
-rw-r--r--spec/frontend/projects/new/components/app_spec.js16
-rw-r--r--spec/frontend/vue_shared/alert_details/router_spec.js35
-rw-r--r--spec/graphql/types/ci/job_type_spec.rb1
-rw-r--r--spec/lib/api/helpers/packages/npm_spec.rb (renamed from spec/support_specs/helpers/packages/npm_spec.rb)0
-rw-r--r--spec/models/project_spec.rb105
-rw-r--r--spec/policies/namespaces/user_namespace_policy_spec.rb28
-rw-r--r--spec/presenters/ci/build_presenter_spec.rb16
-rw-r--r--spec/requests/api/graphql/ci/job_spec.rb3
-rw-r--r--spec/requests/import/gitlab_projects_controller_spec.rb12
-rw-r--r--spec/services/groups/transfer_service_spec.rb53
-rw-r--r--spec/services/import/bitbucket_server_service_spec.rb2
-rw-r--r--spec/services/import/fogbugz_service_spec.rb2
-rw-r--r--spec/services/import/github_service_spec.rb2
-rw-r--r--spec/services/packages/npm/create_package_service_spec.rb91
-rw-r--r--spec/services/projects/create_service_spec.rb17
-rw-r--r--spec/services/projects/transfer_service_spec.rb26
-rw-r--r--spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb13
-rw-r--r--spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb22
86 files changed, 931 insertions, 330 deletions
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index be983b177b2..3484738ebf0 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -140,13 +140,7 @@ retrieve-frontend-fixtures:
script:
- source scripts/gitlab_component_helpers.sh
- |
- if [[ -d "tmp/tests/frontend" ]]; then
- # Remove tmp/tests/frontend/ except on the first parallelized job so that depending
- # jobs don't download the exact same artifact multiple times.
- if [[ -n "${CI_NODE_INDEX:-}" ]] && [[ "${CI_NODE_INDEX}" -ne 1 ]]; then
- echoinfo "INFO: Removing 'tmp/tests/frontend' as we're on node ${CI_NODE_INDEX}.";
- rm -rf "tmp/tests/frontend";
- fi
+ if check_fixtures_reuse; then
exit 0
else
echo "No frontend fixtures directory, generating frontend fixtures."
diff --git a/app/assets/javascripts/import_entities/components/group_dropdown.vue b/app/assets/javascripts/import_entities/components/group_dropdown.vue
index 5b9e80f9d68..1c31c04a416 100644
--- a/app/assets/javascripts/import_entities/components/group_dropdown.vue
+++ b/app/assets/javascripts/import_entities/components/group_dropdown.vue
@@ -4,7 +4,7 @@ import { debounce } from 'lodash';
import { s__ } from '~/locale';
import { createAlert } from '~/alert';
-import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
+import searchNamespacesWhereUserCanImportProjectsQuery from '~/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql';
import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
@@ -28,7 +28,7 @@ export default {
},
apollo: {
namespaces: {
- query: searchNamespacesWhereUserCanCreateProjectsQuery,
+ query: searchNamespacesWhereUserCanImportProjectsQuery,
variables() {
return {
search: this.searchTerm,
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
index 2e6e7cddf8f..246d27d3b94 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
@@ -24,7 +24,7 @@ import { getGroupPathAvailability } from '~/rest_api';
import axios from '~/lib/utils/axios_utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { helpPagePath } from '~/helpers/help_page_helper';
-import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
+import searchNamespacesWhereUserCanImportProjectsQuery from '~/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { STATUSES } from '../../constants';
@@ -118,7 +118,7 @@ export default {
},
},
availableNamespaces: {
- query: searchNamespacesWhereUserCanCreateProjectsQuery,
+ query: searchNamespacesWhereUserCanImportProjectsQuery,
update(data) {
return data.currentUser.groups.nodes;
},
diff --git a/app/assets/javascripts/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql b/app/assets/javascripts/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql
new file mode 100644
index 00000000000..8c41f7116b3
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql
@@ -0,0 +1,18 @@
+query searchNamespacesWhereUserCanImportProjects($search: String) {
+ currentUser {
+ id
+ groups(permissionScope: IMPORT_PROJECTS, search: $search) {
+ nodes {
+ id
+ fullPath
+ name
+ visibility
+ webUrl
+ }
+ }
+ namespace {
+ id
+ fullPath
+ }
+ }
+}
diff --git a/app/assets/javascripts/issues/index.js b/app/assets/javascripts/issues/index.js
index 61531880842..4d2df9e3602 100644
--- a/app/assets/javascripts/issues/index.js
+++ b/app/assets/javascripts/issues/index.js
@@ -47,7 +47,7 @@ export function initForm() {
mountMilestoneDropdown();
}
-export function initShow() {
+export function initShow({ notesParams } = {}) {
const el = document.getElementById('js-issuable-app');
if (!el) {
@@ -71,7 +71,7 @@ export function initShow() {
new ZenMode(); // eslint-disable-line no-new
initIssuableHeaderWarnings(store);
initIssuableSidebar();
- initNotesApp();
+ initNotesApp(notesParams);
initRelatedMergeRequests();
initSentryErrorStackTrace();
diff --git a/app/assets/javascripts/ml/experiment_tracking/constants.js b/app/assets/javascripts/ml/experiment_tracking/constants.js
index 11cf321ad51..f18fbc7e2cd 100644
--- a/app/assets/javascripts/ml/experiment_tracking/constants.js
+++ b/app/assets/javascripts/ml/experiment_tracking/constants.js
@@ -1,7 +1,5 @@
import { s__ } from '~/locale';
-export const EMPTY_STATE_SVG = '/assets/illustrations/empty-state/empty-dag-md.svg';
-
export const FEATURE_NAME = s__('MlExperimentTracking|Machine learning experiment tracking');
export const FEATURE_FEEDBACK_ISSUE = 'https://gitlab.com/gitlab-org/gitlab/-/issues/381660';
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index.vue b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index.vue
index 4f2b8db3c00..eb78c65fd2a 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index.vue
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index.vue
@@ -2,11 +2,7 @@
import { GlTableLite, GlEmptyState, GlLink } from '@gitlab/ui';
import IncubationAlert from '~/vue_shared/components/incubation/incubation_alert.vue';
import Pagination from '~/vue_shared/components/incubation/pagination.vue';
-import {
- FEATURE_NAME,
- FEATURE_FEEDBACK_ISSUE,
- EMPTY_STATE_SVG,
-} from '~/ml/experiment_tracking/constants';
+import { FEATURE_NAME, FEATURE_FEEDBACK_ISSUE } from '~/ml/experiment_tracking/constants';
import * as constants from '~/ml/experiment_tracking/routes/experiments/index/constants';
import * as translations from '~/ml/experiment_tracking/routes/experiments/index/translations';
@@ -28,6 +24,10 @@ export default {
type: Object,
required: true,
},
+ emptyStateSvgPath: {
+ type: String,
+ required: true,
+ },
},
tableFields: constants.EXPERIMENTS_TABLE_FIELDS,
i18n: translations,
@@ -45,7 +45,6 @@ export default {
constants: {
FEATURE_NAME,
FEATURE_FEEDBACK_ISSUE,
- EMPTY_STATE_SVG,
...constants,
},
};
@@ -78,7 +77,7 @@ export default {
:title="$options.i18n.EMPTY_STATE_TITLE_LABEL"
:primary-button-text="$options.i18n.CREATE_NEW_LABEL"
:primary-button-link="$options.constants.CREATE_EXPERIMENT_HELP_PATH"
- :svg-path="$options.constants.EMPTY_STATE_SVG"
+ :svg-path="emptyStateSvgPath"
:description="$options.i18n.EMPTY_STATE_DESCRIPTION_LABEL"
class="gl-py-8"
/>
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue
index acb5fc7cad2..e8118867f7d 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue
@@ -3,11 +3,7 @@ import { GlTableLite, GlLink, GlEmptyState } from '@gitlab/ui';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
-import {
- FEATURE_NAME,
- FEATURE_FEEDBACK_ISSUE,
- EMPTY_STATE_SVG,
-} from '~/ml/experiment_tracking/constants';
+import { FEATURE_NAME, FEATURE_FEEDBACK_ISSUE } from '~/ml/experiment_tracking/constants';
import { queryToObject, setUrlParams, visitUrl } from '~/lib/utils/url_utility';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import KeysetPagination from '~/vue_shared/components/incubation/pagination.vue';
@@ -54,6 +50,10 @@ export default {
type: Object,
required: true,
},
+ emptyStateSvgPath: {
+ type: String,
+ required: true,
+ },
},
data() {
const query = queryToObject(window.location.search);
@@ -159,7 +159,6 @@ export default {
FEATURE_NAME,
FEATURE_FEEDBACK_ISSUE,
CREATE_CANDIDATE_HELP_PATH,
- EMPTY_STATE_SVG,
},
};
</script>
@@ -227,7 +226,7 @@ export default {
:title="$options.i18n.EMPTY_STATE_TITLE_LABEL"
:primary-button-text="$options.i18n.CREATE_NEW_LABEL"
:primary-button-link="$options.constants.CREATE_CANDIDATE_HELP_PATH"
- :svg-path="$options.constants.EMPTY_STATE_SVG"
+ :svg-path="emptyStateSvgPath"
:description="$options.i18n.EMPTY_STATE_DESCRIPTION_LABEL"
class="gl-py-8"
/>
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index cf7207d260d..a499d94db39 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -1,14 +1,13 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { apolloProvider } from '~/graphql_shared/issuable_client';
-import { convertToGraphQLId } from '~/graphql_shared/utils';
import { parseBoolean } from '~/lib/utils/common_utils';
import { getLocationHash } from '~/lib/utils/url_utility';
import NotesApp from './components/notes_app.vue';
import { store } from './stores';
import { getNotesFilterData } from './utils/get_notes_filter_data';
-export default () => {
+export default ({ editorAiActions = [] } = {}) => {
const el = document.getElementById('js-vue-notes');
if (!el) {
return;
@@ -60,7 +59,7 @@ export default () => {
showTimelineViewToggle,
reportAbusePath: notesDataset.reportAbusePath,
newCommentTemplatePath: notesDataset.newCommentTemplatePath,
- resourceGlobalId: convertToGraphQLId(noteableData.noteableType, noteableData.id),
+ editorAiActions: editorAiActions.map((factory) => factory(notesDataset)),
},
data() {
return {
diff --git a/app/assets/javascripts/pages/admin/jobs/components/constants.js b/app/assets/javascripts/pages/admin/jobs/components/constants.js
index 84be895e194..61d5e329fc0 100644
--- a/app/assets/javascripts/pages/admin/jobs/components/constants.js
+++ b/app/assets/javascripts/pages/admin/jobs/components/constants.js
@@ -1,5 +1,5 @@
import { s__, __ } from '~/locale';
-import { DEFAULT_FIELDS } from '~/jobs/components/table/constants';
+import { DEFAULT_FIELDS, RAW_TEXT_WARNING } from '~/jobs/components/table/constants';
export const CANCEL_JOBS_MODAL_ID = 'cancel-jobs-modal';
export const CANCEL_JOBS_MODAL_TITLE = s__('AdminArea|Are you sure?');
@@ -19,3 +19,5 @@ export const DEFAULT_FIELDS_ADMIN = [
{ key: 'runner', label: __('Runner'), columnClass: 'gl-w-15p' },
...DEFAULT_FIELDS.slice(2),
];
+
+export const RAW_TEXT_WARNING_ADMIN = RAW_TEXT_WARNING;
diff --git a/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue b/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue
index 7057e71aefe..da6739aad8b 100644
--- a/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue
+++ b/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue
@@ -1,13 +1,15 @@
<script>
import { GlAlert, GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
-import { queryToObject } from '~/lib/utils/url_utility';
+import { setUrlParams, updateHistory, queryToObject } from '~/lib/utils/url_utility';
import { validateQueryString } from '~/jobs/components/filtered_search/utils';
import JobsTable from '~/jobs/components/table/jobs_table.vue';
import JobsTableTabs from '~/jobs/components/table/jobs_table_tabs.vue';
+import JobsFilteredSearch from '~/jobs/components/filtered_search/jobs_filtered_search.vue';
import JobsTableEmptyState from '~/jobs/components/table/jobs_table_empty_state.vue';
-import { DEFAULT_FIELDS_ADMIN } from '../constants';
+import { createAlert } from '~/alert';
import JobsSkeletonLoader from '../jobs_skeleton_loader.vue';
+import { DEFAULT_FIELDS_ADMIN, RAW_TEXT_WARNING_ADMIN } from '../constants';
import GetAllJobs from './graphql/queries/get_all_jobs.query.graphql';
import CancelableJobs from './graphql/queries/get_cancelable_jobs_count.query.graphql';
@@ -16,10 +18,13 @@ export default {
jobsFetchErrorMsg: __('There was an error fetching the jobs.'),
loadingAriaLabel: __('Loading'),
},
+ filterSearchBoxStyles:
+ 'gl-my-0 gl-p-5 gl-bg-gray-10 gl-text-gray-900 gl-border-b gl-border-gray-100',
components: {
JobsSkeletonLoader,
JobsTableEmptyState,
GlAlert,
+ JobsFilteredSearch,
JobsTable,
JobsTableTabs,
GlIntersectionObserver,
@@ -101,6 +106,9 @@ export default {
return validateQueryString(queryStringObject);
},
+ showFilteredSearch() {
+ return !this.scope;
+ },
jobsCount() {
return this.jobs.count;
},
@@ -140,6 +148,44 @@ export default {
});
}
},
+ filterJobsBySearch(filters) {
+ this.infiniteScrollingTriggered = false;
+ this.filterSearchTriggered = true;
+
+ // all filters have been cleared reset query param
+ // and refetch jobs/count with defaults
+ if (!filters.length) {
+ updateHistory({
+ url: setUrlParams({ statuses: null }, window.location.href, true),
+ });
+
+ this.$apollo.queries.jobs.refetch({ statuses: null });
+
+ return;
+ }
+
+ // Eventually there will be more tokens available
+ // this code is written to scale for those tokens
+ filters.forEach((filter) => {
+ // Raw text input in filtered search does not have a type
+ // when a user enters raw text we alert them that it is
+ // not supported and we do not make an additional API call
+ if (!filter.type) {
+ createAlert({
+ message: RAW_TEXT_WARNING_ADMIN,
+ type: 'warning',
+ });
+ }
+
+ if (filter.type === 'status') {
+ updateHistory({
+ url: setUrlParams({ statuses: filter.value.data }, window.location.href, true),
+ });
+
+ this.$apollo.queries.jobs.refetch({ statuses: filter.value.data });
+ }
+ });
+ },
},
};
</script>
@@ -157,6 +203,13 @@ export default {
@fetchJobsByStatus="fetchJobsByStatus"
/>
+ <div v-if="showFilteredSearch" :class="$options.filterSearchBoxStyles">
+ <jobs-filtered-search
+ :query-string="validatedQueryString"
+ @filterJobsBySearch="filterJobsBySearch"
+ />
+ </div>
+
<jobs-skeleton-loader v-if="showSkeletonLoader" class="gl-mt-5" />
<jobs-table-empty-state v-else-if="showEmptyState" />
diff --git a/app/assets/javascripts/pages/projects/ml/experiments/index/index.js b/app/assets/javascripts/pages/projects/ml/experiments/index/index.js
index e9ffd4b528b..b054022b6d6 100644
--- a/app/assets/javascripts/pages/projects/ml/experiments/index/index.js
+++ b/app/assets/javascripts/pages/projects/ml/experiments/index/index.js
@@ -8,9 +8,11 @@ const initIndexMlExperiments = () => {
return undefined;
}
+ const { experiments, pageInfo, emptyStateSvgPath } = element.dataset;
const props = {
- experiments: JSON.parse(element.dataset.experiments),
- pageInfo: convertObjectPropsToCamelCase(JSON.parse(element.dataset.pageInfo)),
+ experiments: JSON.parse(experiments),
+ pageInfo: convertObjectPropsToCamelCase(JSON.parse(pageInfo)),
+ emptyStateSvgPath,
};
return new Vue({
diff --git a/app/assets/javascripts/pages/projects/ml/experiments/show/index.js b/app/assets/javascripts/pages/projects/ml/experiments/show/index.js
index f50763151ef..b3f15a9f65e 100644
--- a/app/assets/javascripts/pages/projects/ml/experiments/show/index.js
+++ b/app/assets/javascripts/pages/projects/ml/experiments/show/index.js
@@ -8,12 +8,15 @@ const initShowExperiment = () => {
return undefined;
}
+ const { experiment, candidates, metrics, params, pageInfo, emptyStateSvgPath } = element.dataset;
+
const props = {
- experiment: JSON.parse(element.dataset.experiment),
- candidates: JSON.parse(element.dataset.candidates),
- metricNames: JSON.parse(element.dataset.metrics),
- paramNames: JSON.parse(element.dataset.params),
- pageInfo: convertObjectPropsToCamelCase(JSON.parse(element.dataset.pageInfo)),
+ experiment: JSON.parse(experiment),
+ candidates: JSON.parse(candidates),
+ metricNames: JSON.parse(metrics),
+ paramNames: JSON.parse(params),
+ pageInfo: convertObjectPropsToCamelCase(JSON.parse(pageInfo)),
+ emptyStateSvgPath,
};
return new Vue({
diff --git a/app/assets/javascripts/projects/new/components/app.vue b/app/assets/javascripts/projects/new/components/app.vue
index 0a160a357e5..2f58d4468be 100644
--- a/app/assets/javascripts/projects/new/components/app.vue
+++ b/app/assets/javascripts/projects/new/components/app.vue
@@ -9,6 +9,7 @@ import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue'
import NewProjectPushTipPopover from './new_project_push_tip_popover.vue';
const CI_CD_PANEL = 'cicd_for_external_repo';
+const IMPORT_PROJECT_PANEL = 'import_project';
const PANELS = [
{
key: 'blank',
@@ -32,7 +33,7 @@ const PANELS = [
},
{
key: 'import',
- name: 'import_project',
+ name: IMPORT_PROJECT_PANEL,
selector: '#import-project-pane',
title: s__('ProjectsNew|Import project'),
description: s__(
@@ -92,6 +93,11 @@ export default {
required: false,
default: '',
},
+ canImportProjects: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
computed: {
@@ -106,7 +112,21 @@ export default {
return breadcrumbs;
},
availablePanels() {
- return this.isCiCdAvailable ? PANELS : PANELS.filter((p) => p.name !== CI_CD_PANEL);
+ if (this.isCiCdAvailable && this.canImportProjects) {
+ return PANELS;
+ }
+
+ return PANELS.filter((panel) => {
+ if (!this.canImportProjects && panel.name === IMPORT_PROJECT_PANEL) {
+ return false;
+ }
+
+ if (!this.isCiCdAvailable && panel.name === CI_CD_PANEL) {
+ return false;
+ }
+
+ return true;
+ });
},
},
diff --git a/app/assets/javascripts/projects/new/index.js b/app/assets/javascripts/projects/new/index.js
index 5ec50355a82..a5a833dc73b 100644
--- a/app/assets/javascripts/projects/new/index.js
+++ b/app/assets/javascripts/projects/new/index.js
@@ -19,6 +19,7 @@ export function initNewProjectCreation() {
parentGroupName,
projectsUrl,
rootPath,
+ canImportProjects,
} = el.dataset;
const props = {
@@ -29,6 +30,7 @@ export function initNewProjectCreation() {
parentGroupName,
projectsUrl,
rootPath,
+ canImportProjects: parseBoolean(canImportProjects),
};
const provide = {
diff --git a/app/assets/javascripts/vue_shared/alert_details/router.js b/app/assets/javascripts/vue_shared/alert_details/router.js
index 26477a3a66a..616d5c259b9 100644
--- a/app/assets/javascripts/vue_shared/alert_details/router.js
+++ b/app/assets/javascripts/vue_shared/alert_details/router.js
@@ -5,26 +5,9 @@ import { joinPaths } from '~/lib/utils/url_utility';
Vue.use(VueRouter);
export default function createRouter(base) {
- const router = new VueRouter({
+ return new VueRouter({
mode: 'history',
base: joinPaths(gon.relative_url_root || '', base),
routes: [{ path: '/:tabId', name: 'tab' }],
});
-
- /*
- Backward-compatible behavior. Redirects hash mode URLs to history mode ones.
- Ex: from #/overview to /overview
- from #/metrics to /metrics
- from #/activity to /activity
- */
- router.beforeEach((to, _, next) => {
- if (to.hash.startsWith('#/')) {
- const path = to.fullPath.substring(2);
- next(path);
- } else {
- next();
- }
- });
-
- return router;
}
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 3486f231b39..75bb622234a 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -15,6 +15,7 @@ import { getModifierKey } from '~/constants';
import { getSelectedFragment } from '~/lib/utils/common_utils';
import { s__, __ } from '~/locale';
import { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm';
+import { updateText } from '~/lib/utils/text_markdown';
import ToolbarButton from './toolbar_button.vue';
import DrawioToolbarButton from './drawio_toolbar_button.vue';
import CommentTemplatesDropdown from './comment_templates_dropdown.vue';
@@ -39,7 +40,7 @@ export default {
newCommentTemplatePath: {
default: null,
},
- resourceGlobalId: { default: null },
+ editorAiActions: { default: () => [] },
},
props: {
previewMarkdown: {
@@ -121,9 +122,6 @@ export default {
const expandText = s__('MarkdownEditor|Click to expand');
return [`<details><summary>${expandText}</summary>`, `{text}`, '</details>'].join('\n');
},
- showAiActions() {
- return this.resourceGlobalId && this.glFeatures.summarizeComments;
- },
},
watch: {
showSuggestPopover() {
@@ -195,6 +193,18 @@ export default {
$gfmForm.find('.div-dropzone').click();
$gfmTextarea.focus();
},
+ insertIntoTextarea(text) {
+ const textArea = this.$el.closest('.md-area')?.querySelector('textarea');
+ if (textArea) {
+ const generatedByText = `${text}\n***\n_${__('This comment was generated using OpenAI')}_`;
+ updateText({
+ textArea,
+ tag: generatedByText,
+ cursorOffset: 0,
+ wrap: false,
+ });
+ }
+ },
},
shortcuts: {
bold: keysFor(BOLD_TEXT),
@@ -275,7 +285,11 @@ export default {
</gl-button>
</gl-popover>
</template>
- <ai-actions-dropdown v-if="showAiActions" :resource-global-id="resourceGlobalId" />
+ <ai-actions-dropdown
+ v-if="editorAiActions.length"
+ :actions="editorAiActions"
+ @input="insertIntoTextarea"
+ />
<toolbar-button
tag="**"
:button-title="
diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb
index 7ef07032913..bcb6aed9e38 100644
--- a/app/controllers/import/base_controller.rb
+++ b/app/controllers/import/base_controller.rb
@@ -18,7 +18,7 @@ class Import::BaseController < ApplicationController
if params[:namespace_id]&.present?
@namespace = Namespace.find_by_id(params[:namespace_id])
- render_404 unless current_user.can?(:create_projects, @namespace)
+ render_404 unless current_user.can?(:import_projects, @namespace)
end
end
end
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index 8a0f4a36781..c933b05e0c4 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -57,7 +57,7 @@ class Import::BitbucketController < Import::BaseController
extra: { user_role: user_role(current_user, target_namespace), import_type: 'bitbucket' }
)
- if current_user.can?(:create_projects, target_namespace)
+ if current_user.can?(:import_projects, target_namespace)
# The token in a session can be expired, we need to get most recent one because
# Bitbucket::Connection class refreshes it.
session[:bitbucket_token] = bitbucket_client.connection.token
@@ -70,7 +70,7 @@ class Import::BitbucketController < Import::BaseController
render json: { errors: project_save_error(project) }, status: :unprocessable_entity
end
else
- render json: { errors: _('This namespace has already been taken! Please choose another one.') }, status: :unprocessable_entity
+ render json: { errors: _('You are not allowed to import projects in this namespace.') }, status: :unprocessable_entity
end
end
diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb
index 047c273969c..2778b97419a 100644
--- a/app/controllers/import/gitea_controller.rb
+++ b/app/controllers/import/gitea_controller.rb
@@ -32,7 +32,7 @@ class Import::GiteaController < Import::GithubController
if params[:namespace_id].present?
@namespace = Namespace.find_by_id(params[:namespace_id])
- render_404 unless current_user.can?(:create_projects, @namespace)
+ render_404 unless current_user.can?(:import_projects, @namespace)
end
end
end
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index bd0c0976729..719cd61e538 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -65,7 +65,7 @@ class Import::GithubController < Import::BaseController
if params[:namespace_id].present?
@namespace = Namespace.find_by_id(params[:namespace_id])
- render_404 unless current_user.can?(:create_projects, @namespace)
+ render_404 unless current_user.can?(:import_projects, @namespace)
end
end
end
diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb
index 9b8c480e529..d1b182a57d8 100644
--- a/app/controllers/import/gitlab_projects_controller.rb
+++ b/app/controllers/import/gitlab_projects_controller.rb
@@ -8,7 +8,7 @@ class Import::GitlabProjectsController < Import::BaseController
def new
@namespace = Namespace.find(project_params[:namespace_id])
- return render_404 unless current_user.can?(:create_projects, @namespace)
+ return render_404 unless current_user.can?(:import_projects, @namespace)
@path = project_params[:path]
end
diff --git a/app/controllers/import/manifest_controller.rb b/app/controllers/import/manifest_controller.rb
index 461ba982969..03884717e54 100644
--- a/app/controllers/import/manifest_controller.rb
+++ b/app/controllers/import/manifest_controller.rb
@@ -20,8 +20,8 @@ class Import::ManifestController < Import::BaseController
def upload
group = Group.find(params[:group_id])
- unless can?(current_user, :create_projects, group)
- @errors = ["You don't have enough permissions to create projects in the selected group"]
+ unless can?(current_user, :import_projects, group)
+ @errors = ["You don't have enough permissions to import projects in the selected group"]
render :new && return
end
diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb
index 41daeddcf7f..208fbc40556 100644
--- a/app/controllers/projects/imports_controller.rb
+++ b/app/controllers/projects/imports_controller.rb
@@ -56,7 +56,7 @@ class Projects::ImportsController < Projects::ApplicationController
end
def require_namespace_project_creation_permission
- render_404 unless can?(current_user, :admin_project, @project) || can?(current_user, :create_projects, @project.namespace)
+ render_404 unless can?(current_user, :admin_project, @project) || can?(current_user, :import_projects, @project.namespace)
end
def redirect_if_progress
diff --git a/app/finders/groups/accepting_project_imports_finder.rb b/app/finders/groups/accepting_project_imports_finder.rb
new file mode 100644
index 00000000000..55d72edf7bb
--- /dev/null
+++ b/app/finders/groups/accepting_project_imports_finder.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Groups
+ class AcceptingProjectImportsFinder
+ def initialize(current_user)
+ @current_user = current_user
+ end
+
+ def execute
+ ::Group.from_union(
+ [
+ current_user.manageable_groups,
+ managable_groups_originating_from_group_shares
+ ]
+ )
+ end
+
+ private
+
+ attr_reader :current_user
+
+ def managable_groups_originating_from_group_shares
+ GroupGroupLink
+ .with_owner_or_maintainer_access
+ .groups_accessible_via(
+ current_user.owned_or_maintainers_groups
+ .select(:id)
+ )
+ end
+ end
+end
diff --git a/app/finders/groups/user_groups_finder.rb b/app/finders/groups/user_groups_finder.rb
index 83e012b3dbe..536b81b2300 100644
--- a/app/finders/groups/user_groups_finder.rb
+++ b/app/finders/groups/user_groups_finder.rb
@@ -39,6 +39,8 @@ module Groups
Groups::AcceptingProjectCreationsFinder.new(target_user).execute # rubocop: disable CodeReuse/Finder
elsif permission_scope_transfer_projects?
Groups::AcceptingProjectTransfersFinder.new(target_user).execute # rubocop: disable CodeReuse/Finder
+ elsif permission_scope_import_projects?
+ Groups::AcceptingProjectImportsFinder.new(target_user).execute # rubocop: disable CodeReuse/Finder
else
target_user.groups
end
@@ -51,5 +53,9 @@ module Groups
def permission_scope_transfer_projects?
params[:permission_scope] == :transfer_projects
end
+
+ def permission_scope_import_projects?
+ params[:permission_scope] == :import_projects
+ end
end
end
diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb
index 1d12c296b2e..253fee19f2e 100644
--- a/app/graphql/types/ci/job_type.rb
+++ b/app/graphql/types/ci/job_type.rb
@@ -7,6 +7,8 @@ module Types
class JobType < BaseObject
graphql_name 'CiJob'
+ present_using ::Ci::BuildPresenter
+
connection_type_class(Types::LimitedCountableConnectionType)
expose_permissions Types::PermissionTypes::Ci::Job
@@ -91,7 +93,7 @@ module Types
description: 'Path to the ref.'
field :retried, GraphQL::Types::Boolean, null: true,
description: 'Indicates that the job has been retried.'
- field :retryable, GraphQL::Types::Boolean, null: false, method: :retryable?,
+ field :retryable, GraphQL::Types::Boolean, null: false,
description: 'Indicates the job can be retried.'
field :scheduled, GraphQL::Types::Boolean, null: false, method: :scheduled?,
description: 'Indicates the job is scheduled.'
@@ -114,14 +116,21 @@ module Types
null: false, resolver_method: :can_play_job?,
description: 'Indicates whether the current user can play the job.'
+ field :failure_message, GraphQL::Types::String, null: true,
+ description: 'Message on why the job failed.'
+
def can_play_job?
object.playable? && Ability.allowed?(current_user, :play_job, object)
end
def kind
- return ::Ci::Build unless [::Ci::Build, ::Ci::Bridge].include?(object.class)
+ return ::Ci::Build unless [::Ci::Build, ::Ci::Bridge].include?(object.build.class)
+
+ object.build.class
+ end
- object.class
+ def retryable
+ object.build.retryable?
end
def pipeline
diff --git a/app/graphql/types/permission_types/group_enum.rb b/app/graphql/types/permission_types/group_enum.rb
index f636d43790f..6d51d94a70d 100644
--- a/app/graphql/types/permission_types/group_enum.rb
+++ b/app/graphql/types/permission_types/group_enum.rb
@@ -10,6 +10,9 @@ module Types
value 'TRANSFER_PROJECTS',
value: :transfer_projects,
description: 'Groups where the user can transfer projects to.'
+ value 'IMPORT_PROJECTS',
+ value: :import_projects,
+ description: 'Groups where the user can import projects to.'
end
end
end
diff --git a/app/models/packages/npm/metadatum.rb b/app/models/packages/npm/metadatum.rb
index a856cd7225f..ccbf056ec7b 100644
--- a/app/models/packages/npm/metadatum.rb
+++ b/app/models/packages/npm/metadatum.rb
@@ -1,6 +1,10 @@
# frozen_string_literal: true
class Packages::Npm::Metadatum < ApplicationRecord
+ MAX_PACKAGE_JSON_SIZE = 20_000
+ MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING = 5_000
+ NUM_FIELDS_FOR_ERROR_TRACKING = 5
+
belongs_to :package, -> { where(package_type: :npm) }, inverse_of: :npm_metadatum
validates :package, presence: true
@@ -20,7 +24,7 @@ class Packages::Npm::Metadatum < ApplicationRecord
end
def ensure_package_json_size
- return if package_json.to_s.size < 20000
+ return if package_json.to_s.size < MAX_PACKAGE_JSON_SIZE
errors.add(:package_json, _('structure is too large'))
end
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index a8eb990b914..83a8f755d61 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -180,6 +180,7 @@ class Packages::Package < ApplicationRecord
scope :order_project_name, -> { joins(:project).reorder('projects.name ASC') }
scope :order_project_name_desc, -> { joins(:project).reorder('projects.name DESC') }
scope :order_by_package_file, -> { joins(:package_files).order('packages_package_files.created_at ASC') }
+ scope :with_npm_scope, ->(scope) { npm.where("name ILIKE :package_name", package_name: "@#{sanitize_sql_like(scope)}/%") }
scope :order_project_path, -> do
keyset_order = keyset_pagination_order(join_class: Project, column_name: :path, direction: :asc)
diff --git a/app/models/project.rb b/app/models/project.rb
index 7aa13a94c8f..c1f5a2315ef 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1177,10 +1177,6 @@ class Project < ApplicationRecord
auto_devops_config[:scope] != :project && !auto_devops_config[:status]
end
- def has_packages?(package_type)
- packages.where(package_type: package_type).exists?
- end
-
def packages_cleanup_policy
super || build_packages_cleanup_policy
end
@@ -2957,6 +2953,12 @@ class Project < ApplicationRecord
).exists?
end
+ def has_namespaced_npm_packages?
+ packages.with_npm_scope(root_namespace.path)
+ .not_pending_destruction
+ .exists?
+ end
+
def default_branch_or_main
return default_branch if default_branch
diff --git a/app/policies/namespaces/user_namespace_policy.rb b/app/policies/namespaces/user_namespace_policy.rb
index 1deeae8241f..bfed61e72d3 100644
--- a/app/policies/namespaces/user_namespace_policy.rb
+++ b/app/policies/namespaces/user_namespace_policy.rb
@@ -11,6 +11,7 @@ module Namespaces
rule { owner | admin }.policy do
enable :owner_access
enable :create_projects
+ enable :import_projects
enable :admin_namespace
enable :read_namespace
enable :read_statistics
@@ -20,9 +21,9 @@ module Namespaces
enable :edit_billing
end
- rule { ~can_create_personal_project }.prevent :create_projects
+ rule { ~can_create_personal_project }.prevent :create_projects, :import_projects
- rule { bot_user_namespace }.prevent :create_projects
+ rule { bot_user_namespace }.prevent :create_projects, :import_projects
rule { (owner | admin) & can?(:create_projects) }.enable :transfer_projects
end
diff --git a/app/presenters/ci/build_presenter.rb b/app/presenters/ci/build_presenter.rb
index 513fcd90cf8..b89e8db334d 100644
--- a/app/presenters/ci/build_presenter.rb
+++ b/app/presenters/ci/build_presenter.rb
@@ -27,6 +27,10 @@ module Ci
scheduled? && scheduled_at && [0, scheduled_at - Time.now].max
end
+ def failure_message
+ callout_failure_message if build.failed?
+ end
+
private
def tooltip_for_badge(status)
diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb
index 7e9fd9dad54..1c8df157716 100644
--- a/app/services/groups/transfer_service.rb
+++ b/app/services/groups/transfer_service.rb
@@ -60,7 +60,7 @@ module Groups
raise_transfer_error(:namespace_with_same_path) if namespace_with_same_path?
raise_transfer_error(:group_contains_images) if group_projects_contain_registry_images?
raise_transfer_error(:cannot_transfer_to_subgroup) if transfer_to_subgroup?
- raise_transfer_error(:group_contains_npm_packages) if group_with_npm_packages?
+ raise_transfer_error(:group_contains_namespaced_npm_packages) if group_with_namespaced_npm_packages?
raise_transfer_error(:no_permissions_to_migrate_crm) if no_permissions_to_migrate_crm?
end
@@ -74,10 +74,11 @@ module Groups
false
end
- def group_with_npm_packages?
+ def group_with_namespaced_npm_packages?
return false unless group.packages_feature_enabled?
- npm_packages = ::Packages::GroupPackagesFinder.new(current_user, group, package_type: :npm).execute
+ npm_packages = ::Packages::GroupPackagesFinder.new(current_user, group, package_type: :npm, preload_pipelines: false).execute
+ npm_packages = npm_packages.with_npm_scope(group.root_ancestor.path)
different_root_ancestor? && npm_packages.exists?
end
@@ -219,7 +220,7 @@ module Groups
invalid_policies: s_("TransferGroup|You don't have enough permissions."),
group_contains_images: s_('TransferGroup|Cannot update the path because there are projects under this group that contain Docker images in their Container Registry. Please remove the images from your projects first and try again.'),
cannot_transfer_to_subgroup: s_('TransferGroup|Cannot transfer group to one of its subgroup.'),
- group_contains_npm_packages: s_('TransferGroup|Group contains projects with NPM packages.'),
+ group_contains_namespaced_npm_packages: s_('TransferGroup|Group contains projects with NPM packages scoped to the current root level group.'),
no_permissions_to_migrate_crm: s_("TransferGroup|Group contains contacts/organizations and you don't have enough permissions to move them to the new root group.")
}.freeze
end
diff --git a/app/services/import/base_service.rb b/app/services/import/base_service.rb
index 6b5adcbc39e..64cf3cfa04a 100644
--- a/app/services/import/base_service.rb
+++ b/app/services/import/base_service.rb
@@ -9,7 +9,7 @@ module Import
end
def authorized?
- can?(current_user, :create_projects, target_namespace)
+ can?(current_user, :import_projects, target_namespace)
end
private
diff --git a/app/services/import/bitbucket_server_service.rb b/app/services/import/bitbucket_server_service.rb
index f7f17f1e53e..5d496dc7cc3 100644
--- a/app/services/import/bitbucket_server_service.rb
+++ b/app/services/import/bitbucket_server_service.rb
@@ -10,7 +10,7 @@ module Import
end
unless authorized?
- return log_and_return_error("You don't have permissions to create this project", :unauthorized)
+ return log_and_return_error("You don't have permissions to import this project", :unauthorized)
end
unless repo
diff --git a/app/services/import/fogbugz_service.rb b/app/services/import/fogbugz_service.rb
index d1003823456..9a8def43312 100644
--- a/app/services/import/fogbugz_service.rb
+++ b/app/services/import/fogbugz_service.rb
@@ -13,8 +13,8 @@ module Import
unless authorized?
return log_and_return_error(
- "You don't have permissions to create this project",
- _("You don't have permissions to create this project"),
+ "You don't have permissions to import this project",
+ _("You don't have permissions to import this project"),
:unauthorized
)
end
diff --git a/app/services/import/github_service.rb b/app/services/import/github_service.rb
index b30c344723d..7e7f7ea9810 100644
--- a/app/services/import/github_service.rb
+++ b/app/services/import/github_service.rb
@@ -103,7 +103,7 @@ module Import
elsif target_namespace.nil?
error(_('Namespace or group to import repository into does not exist.'), :unprocessable_entity)
elsif !authorized?
- error(_('This namespace has already been taken. Choose a different one.'), :unprocessable_entity)
+ error(_('You are not allowed to import projects in this namespace.'), :unprocessable_entity)
elsif oversized?
error(oversize_error_message, :unprocessable_entity)
end
diff --git a/app/services/packages/npm/create_package_service.rb b/app/services/packages/npm/create_package_service.rb
index 33a7736dc95..b5a0a22a24e 100644
--- a/app/services/packages/npm/create_package_service.rb
+++ b/app/services/packages/npm/create_package_service.rb
@@ -35,11 +35,21 @@ module Packages
::Packages::CreateDependencyService.new(package, package_dependencies).execute
::Packages::Npm::CreateTagService.new(package, dist_tag).execute
- package.create_npm_metadatum!(package_json: package_json)
+ create_npm_metadatum!(package)
package
end
+ def create_npm_metadatum!(package)
+ package.create_npm_metadatum!(package_json: package_json)
+ rescue ActiveRecord::RecordInvalid => e
+ if package.npm_metadatum && package.npm_metadatum.errors.added?(:package_json, 'structure is too large')
+ Gitlab::ErrorTracking.track_exception(e, field_sizes: field_sizes_for_error_tracking)
+ end
+
+ raise
+ end
+
def current_package_exists?
project.packages
.npm
@@ -125,6 +135,35 @@ module Packages
def lease_timeout
DEFAULT_LEASE_TIMEOUT
end
+
+ def field_sizes
+ strong_memoize(:field_sizes) do
+ package_json.transform_values do |value|
+ value.to_s.size
+ end
+ end
+ end
+
+ def filtered_field_sizes
+ strong_memoize(:filtered_field_sizes) do
+ field_sizes.select do |_, size|
+ size >= ::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING
+ end
+ end
+ end
+
+ def largest_fields
+ strong_memoize(:largest_fields) do
+ field_sizes
+ .sort_by { |a| a[1] }
+ .reverse[0..::Packages::Npm::Metadatum::NUM_FIELDS_FOR_ERROR_TRACKING - 1]
+ .to_h
+ end
+ end
+
+ def field_sizes_for_error_tracking
+ filtered_field_sizes.empty? ? largest_fields : filtered_field_sizes
+ end
end
end
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index cbea44d6aff..63b050faf9c 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -58,6 +58,7 @@ module Projects
return @project if @project.errors.any?
validate_create_permissions
+ validate_import_permissions
return @project if @project.errors.any?
@relations_block&.call(@project)
@@ -98,6 +99,13 @@ module Projects
@project.errors.add(:namespace, "is not valid")
end
+ def validate_import_permissions
+ return unless @project.import?
+ return if current_user.can?(:import_projects, parent_namespace)
+
+ @project.errors.add(:user, 'is not allowed to import projects')
+ end
+
def after_create_actions
log_info("#{current_user.name} created a new project \"#{@project.full_name}\"")
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index ed99c69be07..4a9d96d266c 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -63,8 +63,8 @@ module Projects
raise TransferError, s_('TransferProject|Project cannot be transferred, because tags are present in its container registry')
end
- if project.has_packages?(:npm) && !new_namespace_has_same_root?(project)
- raise TransferError, s_("TransferProject|Root namespace can't be updated if project has NPM packages")
+ if !new_namespace_has_same_root?(project) && project.has_namespaced_npm_packages?
+ raise TransferError, s_("TransferProject|Root namespace can't be updated if the project has NPM packages scoped to the current root level namespace.")
end
proceed_to_transfer
diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml
index 1a0f4132d49..dde0b301c63 100644
--- a/app/views/admin/abuse_reports/_abuse_report.html.haml
+++ b/app/views/admin/abuse_reports/_abuse_report.html.haml
@@ -7,7 +7,7 @@
- if user
= link_to user.name, user
.light.small
- = _('Joined %{time_ago}').html_safe % { time_ago: time_ago_with_tooltip(user.created_at) }
+ = html_escape(_('Joined %{time_ago}')) % { time_ago: time_ago_with_tooltip(user.created_at).html_safe }
- else
= _('(removed)')
%td
diff --git a/app/views/groups/settings/access_tokens/index.html.haml b/app/views/groups/settings/access_tokens/index.html.haml
index 8435f32db49..bf78b2f8e68 100644
--- a/app/views/groups/settings/access_tokens/index.html.haml
+++ b/app/views/groups/settings/access_tokens/index.html.haml
@@ -12,15 +12,15 @@
- if current_user.can?(:create_resource_access_tokens, @group)
= _('Generate group access tokens scoped to this group for your applications that need access to the GitLab API.')
%p
- = _('You can also use group access tokens with Git to authenticate over HTTP(S). %{link_start}Learn more.%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
+ = html_escape(_('You can also use group access tokens with Git to authenticate over HTTP(S). %{link_start}Learn more.%{link_end}')) % { link_start: link_start, link_end: '</a>'.html_safe }
- else
- = _('Group access token creation is disabled in this group. You can still use and manage existing tokens. %{link_start}Learn more.%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
+ = html_escape(_('Group access token creation is disabled in this group. You can still use and manage existing tokens. %{link_start}Learn more.%{link_end}')) % { link_start: link_start, link_end: '</a>'.html_safe }
%p
- root_group = @group.root_ancestor
- if current_user.can?(:admin_group, root_group)
- group_settings_link = edit_group_path(root_group)
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: group_settings_link }
- = _('You can enable group access token creation in %{link_start}group settings%{link_end}.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
+ = html_escape(_('You can enable group access token creation in %{link_start}group settings%{link_end}.')) % { link_start: link_start, link_end: '</a>'.html_safe }
.col-lg-8
#js-new-access-token-app{ data: { access_token_type: type } }
diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml
index 67b87f842f9..389cf80f954 100644
--- a/app/views/groups/settings/ci_cd/show.html.haml
+++ b/app/views/groups/settings/ci_cd/show.html.haml
@@ -47,7 +47,7 @@
- quickstart_url = help_page_path('topics/autodevops/cloud_deployments/auto_devops_with_gke')
- auto_devops_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: auto_devops_url }
- quickstart_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: quickstart_url }
- = s_('AutoDevOps|%{auto_devops_start}Automate building, testing, and deploying%{auto_devops_end} your applications based on your continuous integration and delivery configuration. %{quickstart_start}How do I get started?%{quickstart_end}').html_safe % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe, quickstart_start: quickstart_start, quickstart_end: '</a>'.html_safe }
+ = html_escape(s_('AutoDevOps|%{auto_devops_start}Automate building, testing, and deploying%{auto_devops_end} your applications based on your continuous integration and delivery configuration. %{quickstart_start}How do I get started?%{quickstart_end}')) % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe, quickstart_start: quickstart_start, quickstart_end: '</a>'.html_safe }
.settings-content
= render 'groups/settings/ci_cd/auto_devops_form', group: @group
diff --git a/app/views/projects/ml/experiments/index.html.haml b/app/views/projects/ml/experiments/index.html.haml
index dd064239e36..612481dbf41 100644
--- a/app/views/projects/ml/experiments/index.html.haml
+++ b/app/views/projects/ml/experiments/index.html.haml
@@ -3,5 +3,6 @@
#js-project-ml-experiments-index{ data: {
experiments: experiments_as_data(@project, @experiments),
- page_info: formatted_page_info(@page_info)
+ page_info: formatted_page_info(@page_info),
+ empty_state_svg_path: image_path('illustrations/empty-state/empty-dag-md.svg'),
} }
diff --git a/app/views/projects/ml/experiments/show.html.haml b/app/views/projects/ml/experiments/show.html.haml
index cfec627d249..d4a05b613c9 100644
--- a/app/views/projects/ml/experiments/show.html.haml
+++ b/app/views/projects/ml/experiments/show.html.haml
@@ -14,5 +14,6 @@
candidates: items,
metrics: metrics,
params: params,
- page_info: page_info
+ page_info: page_info,
+ empty_state_svg_path: image_path('illustrations/empty-state/empty-dag-md.svg'),
} }
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index e64ed2c7b8f..52ac8b58c9a 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -17,7 +17,8 @@
root_path: root_path,
parent_group_url: @project.parent && group_url(@project.parent),
parent_group_name: @project.parent&.name,
- projects_url: dashboard_projects_url } }
+ projects_url: dashboard_projects_url,
+ can_import_projects: params[:namespace_id].presence ? current_user.can?(:import_projects, @namespace).to_s : 'true' } }
.row{ 'v-cloak': true }
#blank-project-pane.tab-pane.active
diff --git a/config/feature_flags/development/exit_registration_verification.yml b/config/feature_flags/development/exit_registration_verification.yml
deleted file mode 100644
index c544ebc2943..00000000000
--- a/config/feature_flags/development/exit_registration_verification.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: exit_registration_verification
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80286
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/352397
-milestone: '14.8'
-type: development
-group: group::activation
-default_enabled: false
diff --git a/config/routes.rb b/config/routes.rb
index ebb0984a008..9c8ad8fe047 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -67,10 +67,7 @@ InitializerConnections.raise_if_new_database_connection do
Gitlab.ee do
resource :company, only: [:new, :create], controller: 'company'
resources :groups_projects, only: [:new, :create] do
- collection do
- post :import
- put :exit
- end
+ post :import, on: :collection
end
draw :verification
end
diff --git a/db/docs/namespace_statistics.yml b/db/docs/namespace_statistics.yml
index e84d5d563f8..4c294db3315 100644
--- a/db/docs/namespace_statistics.yml
+++ b/db/docs/namespace_statistics.yml
@@ -4,7 +4,8 @@ classes:
- NamespaceStatistics
feature_categories:
- application_instrumentation
-description: TODO
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/48d8bdca0493056a717cd7d9fee2e8b51d6b0502
+- consumables_cost_management
+description: Stores usage statistics for both CI minutes and a limited set of storage types for a given namespace. This should not be confused with namespace_root_storage_statistics table which holds statistics across more storage types for a group.
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/965
milestone: '9.0'
gitlab_schema: gitlab_main
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 78a9a8a9e67..d6fc9e60020 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -12027,6 +12027,7 @@ CI/CD variables for a GitLab instance.
| <a id="cijobdownstreampipeline"></a>`downstreamPipeline` | [`Pipeline`](#pipeline) | Downstream pipeline for a bridge. |
| <a id="cijobduration"></a>`duration` | [`Int`](#int) | Duration of the job in seconds. |
| <a id="cijoberasedat"></a>`erasedAt` | [`Time`](#time) | When the job was erased. |
+| <a id="cijobfailuremessage"></a>`failureMessage` | [`String`](#string) | Message on why the job failed. |
| <a id="cijobfinishedat"></a>`finishedAt` | [`Time`](#time) | When a job has finished running. |
| <a id="cijobid"></a>`id` | [`JobID`](#jobid) | ID of the job. |
| <a id="cijobkind"></a>`kind` | [`CiJobKind!`](#cijobkind) | Indicates the type of job. |
@@ -23847,6 +23848,7 @@ User permission on groups.
| Value | Description |
| ----- | ----------- |
| <a id="grouppermissioncreate_projects"></a>`CREATE_PROJECTS` | Groups where the user can create projects. |
+| <a id="grouppermissionimport_projects"></a>`IMPORT_PROJECTS` | Groups where the user can import projects to. |
| <a id="grouppermissiontransfer_projects"></a>`TRANSFER_PROJECTS` | Groups where the user can transfer projects to. |
### `GroupReleaseSort`
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 2936d4d21b9..196f692d6f6 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -33134,6 +33134,9 @@ msgstr ""
msgid "Please wait a moment, this page will automatically refresh when ready."
msgstr ""
+msgid "Please wait for the current action to complete"
+msgstr ""
+
msgid "Please wait while we connect to your repository. Refresh at will."
msgstr ""
@@ -45497,9 +45500,6 @@ msgstr ""
msgid "This namespace has already been taken! Please choose another one."
msgstr ""
-msgid "This namespace has already been taken. Choose a different one."
-msgstr ""
-
msgid "This only applies to repository indexing operations."
msgstr ""
@@ -46609,7 +46609,7 @@ msgstr ""
msgid "TransferGroup|Group contains contacts/organizations and you don't have enough permissions to move them to the new root group."
msgstr ""
-msgid "TransferGroup|Group contains projects with NPM packages."
+msgid "TransferGroup|Group contains projects with NPM packages scoped to the current root level group."
msgstr ""
msgid "TransferGroup|Group is already a root group."
@@ -46645,7 +46645,7 @@ msgstr ""
msgid "TransferProject|Project with same name or path in target namespace already exists"
msgstr ""
-msgid "TransferProject|Root namespace can't be updated if project has NPM packages"
+msgid "TransferProject|Root namespace can't be updated if the project has NPM packages scoped to the current root level namespace."
msgstr ""
msgid "TransferProject|You don't have permission to transfer projects into that namespace."
@@ -50553,6 +50553,9 @@ msgstr ""
msgid "You are not allowed to download code from this project."
msgstr ""
+msgid "You are not allowed to import projects in this namespace."
+msgstr ""
+
msgid "You are not allowed to log in using password"
msgstr ""
@@ -50868,7 +50871,7 @@ msgstr ""
msgid "You don't have permission to view this epic"
msgstr ""
-msgid "You don't have permissions to create this project"
+msgid "You don't have permissions to import this project"
msgstr ""
msgid "You don't have sufficient permission to perform this action."
diff --git a/qa/qa/support/page/logging.rb b/qa/qa/support/page/logging.rb
index ad8d63ec856..ccc5e04d672 100644
--- a/qa/qa/support/page/logging.rb
+++ b/qa/qa/support/page/logging.rb
@@ -36,9 +36,7 @@ module QA
def find_element(name, **kwargs)
log("finding :#{name} with args #{kwargs}")
-
- element = super
-
+ element = log_slow_code(name, **kwargs) { super }
log("found :#{name}")
element
@@ -46,9 +44,7 @@ module QA
def all_elements(name, **kwargs)
log("finding all :#{name} with args #{kwargs}")
-
- elements = super
-
+ elements = log_slow_code(name, **kwargs) { super }
log("found #{elements.size} :#{name}") if elements
elements
@@ -88,8 +84,7 @@ module QA
log(msg.join(' '), :info)
log("with args #{kwargs}")
-
- super
+ log_slow_code(name, **kwargs) { super }
end
def act_via_capybara(method, locator, **kwargs)
@@ -113,32 +108,28 @@ module QA
end
def has_element?(name, **kwargs)
- found = super
-
+ found = log_slow_code(name, **kwargs) { super }
log_has_element_or_not('has_element?', name, found, **kwargs)
found
end
def has_no_element?(name, **kwargs)
- found = super
-
+ found = log_slow_code(name, **kwargs) { super }
log_has_element_or_not('has_no_element?', name, found, **kwargs)
found
end
def has_text?(text, **kwargs)
- found = super
-
+ found = log_slow_code(text, **kwargs) { super }
log(%(has_text?('#{text}', wait: #{kwargs[:wait] || Capybara.default_max_wait_time}) returned #{found}))
found
end
def has_no_text?(text, **kwargs)
- found = super
-
+ found = log_slow_code(text, **kwargs) { super }
log(%(has_no_text?('#{text}', wait: #{kwargs[:wait] || Capybara.default_max_wait_time}) returned #{found}))
found
@@ -146,13 +137,11 @@ module QA
def finished_loading?(wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
log('waiting for loading to complete...')
- now = Time.now
-
- loaded = super
-
- log("loading complete after #{Time.now - now} seconds")
+ log_slow_code { super }
+ end
- loaded
+ def wait_for_requests(skip_finished_loading_check: false, skip_resp_code_check: false)
+ log_slow_code { super }
end
def wait_for_animated_element(name)
@@ -209,6 +198,21 @@ module QA
log(msg.compact.join(' '))
end
+
+ # Prints warning log if code duration is slower than threshold
+ # @param [String (frozen)] paramInfo is info relating to the slow element
+ def log_slow_code(param_info = '', **kwargs)
+ starting = kwargs.fetch(:starting_time, Time.now)
+ result = yield
+ ending = kwargs.fetch(:ending_time, Time.now)
+ duration = (ending - starting).round(3)
+ if duration > kwargs.fetch(:log_slow_threshold, 0.5)
+ caller_method_name = caller_locations(1, 1).first.label
+ QA::Runtime::Logger.warn("Potentially Slow Code '#{caller_method_name} #{param_info}' took #{duration}s")
+ end
+
+ result
+ end
end
end
end
diff --git a/qa/spec/page/logging_spec.rb b/qa/spec/page/logging_spec.rb
index 1a82cda2585..646b77a7f84 100644
--- a/qa/spec/page/logging_spec.rb
+++ b/qa/spec/page/logging_spec.rb
@@ -62,6 +62,20 @@ RSpec.describe QA::Support::Page::Logging do
.to output(/finding :element with args {:class=>"active"}/).to_stdout_from_any_process
end
+ it 'logs a warning if find_element is slow' do
+ starting = Time.now
+ ending = starting + 1.4
+ expected_msg = /Potentially Slow Code 'find_element element' took 1.4s/
+
+ # verify logs a warning message to indicate potentially slow code lookups
+ expect { subject.find_element(:element, starting_time: starting, ending_time: ending) }
+ .to output(expected_msg).to_stdout_from_any_process
+
+ # verify it doesn't log a warning message if within allowed limits
+ expect { subject.find_element(:element, starting_time: starting, ending_time: ending, log_slow_threshold: 1.5) }
+ .not_to output(expected_msg).to_stdout_from_any_process
+ end
+
it 'logs click_element' do
expect { subject.click_element(:element) }
.to output(/clicking :element/).to_stdout_from_any_process
@@ -127,8 +141,6 @@ RSpec.describe QA::Support::Page::Logging do
it 'logs finished_loading?' do
expect { subject.finished_loading? }
.to output(/waiting for loading to complete\.\.\./).to_stdout_from_any_process
- expect { subject.finished_loading? }
- .to output(/loading complete after .* seconds$/).to_stdout_from_any_process
end
it 'logs within_element' do
diff --git a/scripts/gitlab_component_helpers.sh b/scripts/gitlab_component_helpers.sh
index 301d4fb5d37..3d5116d6cc2 100644
--- a/scripts/gitlab_component_helpers.sh
+++ b/scripts/gitlab_component_helpers.sh
@@ -180,6 +180,32 @@ function check_fixtures_download() {
fi
}
+function check_fixtures_reuse() {
+ if [[ "${REUSE_FRONTEND_FIXTURES_ENABLED:-}" != "true" ]]; then
+ rm -rf "tmp/tests/frontend";
+ return 1
+ fi
+
+ # Note: Currently, reusing frontend fixtures is only supported in EE.
+ # Other projects will be supported through this issue in the future: https://gitlab.com/gitlab-org/gitlab/-/issues/393615.
+ if [[ "${CI_PROJECT_NAME}" != "gitlab" ]] || [[ "${CI_JOB_NAME}" =~ "foss" ]]; then
+ rm -rf "tmp/tests/frontend";
+ return 1
+ fi
+
+ if [[ -d "tmp/tests/frontend" ]]; then
+ # Remove tmp/tests/frontend/ except on the first parallelized job so that depending
+ # jobs don't download the exact same artifact multiple times.
+ if [[ -n "${CI_NODE_INDEX:-}" ]] && [[ "${CI_NODE_INDEX}" -ne 1 ]]; then
+ echoinfo "INFO: Removing 'tmp/tests/frontend' as we're on node ${CI_NODE_INDEX}.";
+ rm -rf "tmp/tests/frontend";
+ fi
+ return 0
+ else
+ return 1
+ fi
+}
+
function create_fixtures_package() {
create_package "${FIXTURES_PACKAGE}" "${FIXTURES_PATH}"
}
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb
index 055c98ebdbc..906cc5cb336 100644
--- a/spec/controllers/import/bitbucket_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Import::BitbucketController do
+RSpec.describe Import::BitbucketController, feature_category: :importers do
include ImportSpecHelper
let(:user) { create(:user) }
@@ -445,5 +445,16 @@ RSpec.describe Import::BitbucketController do
)
end
end
+
+ context 'when user can not import projects' do
+ let!(:other_namespace) { create(:group, name: 'other_namespace').tap { |other_namespace| other_namespace.add_developer(user) } }
+
+ it 'returns 422 response' do
+ post :create, params: { target_namespace: other_namespace.name }, format: :json
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(response.parsed_body['errors']).to eq('You are not allowed to import projects in this namespace.')
+ end
+ end
end
end
diff --git a/spec/controllers/import/bitbucket_server_controller_spec.rb b/spec/controllers/import/bitbucket_server_controller_spec.rb
index ac56d3af54f..b2a56423253 100644
--- a/spec/controllers/import/bitbucket_server_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_server_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Import::BitbucketServerController do
+RSpec.describe Import::BitbucketServerController, feature_category: :importers do
let(:user) { create(:user) }
let(:project_key) { 'test-project' }
let(:repo_slug) { 'some-repo' }
diff --git a/spec/controllers/import/fogbugz_controller_spec.rb b/spec/controllers/import/fogbugz_controller_spec.rb
index e2d59fc213a..40a5c59fa2d 100644
--- a/spec/controllers/import/fogbugz_controller_spec.rb
+++ b/spec/controllers/import/fogbugz_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Import::FogbugzController do
+RSpec.describe Import::FogbugzController, feature_category: :importers do
include ImportSpecHelper
let(:user) { create(:user) }
diff --git a/spec/controllers/import/gitea_controller_spec.rb b/spec/controllers/import/gitea_controller_spec.rb
index 568712d29cb..7466ffb2393 100644
--- a/spec/controllers/import/gitea_controller_spec.rb
+++ b/spec/controllers/import/gitea_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Import::GiteaController do
+RSpec.describe Import::GiteaController, feature_category: :importers do
include ImportSpecHelper
let(:provider) { :gitea }
diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb
index 7b3978297fb..2c09f8c010e 100644
--- a/spec/controllers/import/gitlab_controller_spec.rb
+++ b/spec/controllers/import/gitlab_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Import::GitlabController do
+RSpec.describe Import::GitlabController, feature_category: :importers do
include ImportSpecHelper
let(:user) { create(:user) }
diff --git a/spec/controllers/import/manifest_controller_spec.rb b/spec/controllers/import/manifest_controller_spec.rb
index 6f805b44e89..23d5d37ed88 100644
--- a/spec/controllers/import/manifest_controller_spec.rb
+++ b/spec/controllers/import/manifest_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Import::ManifestController, :clean_gitlab_redis_shared_state do
+RSpec.describe Import::ManifestController, :clean_gitlab_redis_shared_state, feature_category: :importers do
include ImportSpecHelper
let_it_be(:user) { create(:user) }
@@ -45,7 +45,7 @@ RSpec.describe Import::ManifestController, :clean_gitlab_redis_shared_state do
end
end
- context 'when the user cannot create projects in the group' do
+ context 'when the user cannot import projects in the group' do
it 'displays an error' do
sign_in(create(:user))
diff --git a/spec/controllers/projects/imports_controller_spec.rb b/spec/controllers/projects/imports_controller_spec.rb
index b4704d56cd9..4502f3d7bd9 100644
--- a/spec/controllers/projects/imports_controller_spec.rb
+++ b/spec/controllers/projects/imports_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ImportsController do
+RSpec.describe Projects::ImportsController, feature_category: :importers do
let(:user) { create(:user) }
let(:project) { create(:project) }
@@ -149,17 +149,7 @@ RSpec.describe Projects::ImportsController do
import_state.update!(status: :started)
end
- context 'when group allows developers to create projects' do
- let(:group) { create(:group, project_creation_level: Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) }
-
- it 'renders template' do
- get :show, params: { namespace_id: project.namespace.to_param, project_id: project }
-
- expect(response).to render_template :show
- end
- end
-
- context 'when group prohibits developers to create projects' do
+ context 'when group prohibits developers to import projects' do
let(:group) { create(:group, project_creation_level: Gitlab::Access::MAINTAINER_PROJECT_ACCESS) }
it 'returns 404 response' do
diff --git a/spec/finders/groups/accepting_project_imports_finder_spec.rb b/spec/finders/groups/accepting_project_imports_finder_spec.rb
new file mode 100644
index 00000000000..4e06c2cbc67
--- /dev/null
+++ b/spec/finders/groups/accepting_project_imports_finder_spec.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::AcceptingProjectImportsFinder, feature_category: :importers do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group_where_direct_owner) { create(:group) }
+ let_it_be(:subgroup_of_group_where_direct_owner) { create(:group, parent: group_where_direct_owner) }
+ let_it_be(:group_where_direct_maintainer) { create(:group) }
+ let_it_be(:group_where_direct_maintainer_but_cant_create_projects) do
+ create(:group, project_creation_level: Gitlab::Access::NO_ONE_PROJECT_ACCESS)
+ end
+
+ let_it_be(:group_where_direct_developer_but_developers_cannot_create_projects) { create(:group) }
+ let_it_be(:group_where_direct_developer) do
+ create(:group, project_creation_level: Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS)
+ end
+
+ let_it_be(:shared_with_group_where_direct_owner_as_owner) { create(:group) }
+
+ let_it_be(:shared_with_group_where_direct_owner_as_developer) do
+ create(:group, project_creation_level: Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS)
+ end
+
+ let_it_be(:shared_with_group_where_direct_owner_as_developer_but_developers_cannot_create_projects) do
+ create(:group)
+ end
+
+ let_it_be(:shared_with_group_where_direct_developer_as_maintainer) do
+ create(:group, project_creation_level: Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS)
+ end
+
+ let_it_be(:shared_with_group_where_direct_owner_as_guest) { create(:group) }
+ let_it_be(:shared_with_group_where_direct_owner_as_maintainer) { create(:group) }
+ let_it_be(:shared_with_group_where_direct_developer_as_owner) do
+ create(:group, project_creation_level: Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS)
+ end
+
+ let_it_be(:subgroup_of_shared_with_group_where_direct_owner_as_maintainer) do
+ create(:group, parent: shared_with_group_where_direct_owner_as_maintainer)
+ end
+
+ before do
+ group_where_direct_owner.add_owner(user)
+ group_where_direct_maintainer.add_maintainer(user)
+ group_where_direct_developer_but_developers_cannot_create_projects.add_developer(user)
+ group_where_direct_developer.add_developer(user)
+
+ create(:group_group_link, :owner,
+ shared_with_group: group_where_direct_owner,
+ shared_group: shared_with_group_where_direct_owner_as_owner
+ )
+
+ create(:group_group_link, :developer,
+ shared_with_group: group_where_direct_owner,
+ shared_group: shared_with_group_where_direct_owner_as_developer_but_developers_cannot_create_projects
+ )
+
+ create(:group_group_link, :maintainer,
+ shared_with_group: group_where_direct_developer,
+ shared_group: shared_with_group_where_direct_developer_as_maintainer
+ )
+
+ create(:group_group_link, :developer,
+ shared_with_group: group_where_direct_owner,
+ shared_group: shared_with_group_where_direct_owner_as_developer
+ )
+
+ create(:group_group_link, :guest,
+ shared_with_group: group_where_direct_owner,
+ shared_group: shared_with_group_where_direct_owner_as_guest
+ )
+
+ create(:group_group_link, :maintainer,
+ shared_with_group: group_where_direct_owner,
+ shared_group: shared_with_group_where_direct_owner_as_maintainer
+ )
+
+ create(:group_group_link, :owner,
+ shared_with_group: group_where_direct_developer_but_developers_cannot_create_projects,
+ shared_group: shared_with_group_where_direct_developer_as_owner
+ )
+ end
+
+ describe '#execute' do
+ subject(:result) { described_class.new(user).execute }
+
+ it 'only returns groups where the user has access to import projects' do
+ expect(result).to match_array([
+ group_where_direct_owner,
+ subgroup_of_group_where_direct_owner,
+ group_where_direct_maintainer,
+ # groups arising from group shares
+ shared_with_group_where_direct_owner_as_owner,
+ shared_with_group_where_direct_owner_as_maintainer,
+ subgroup_of_shared_with_group_where_direct_owner_as_maintainer
+ ])
+
+ expect(result).not_to include(group_where_direct_developer)
+ expect(result).not_to include(shared_with_group_where_direct_developer_as_owner)
+ expect(result).not_to include(shared_with_group_where_direct_developer_as_maintainer)
+ expect(result).not_to include(shared_with_group_where_direct_owner_as_developer)
+ end
+ end
+end
diff --git a/spec/finders/groups/user_groups_finder_spec.rb b/spec/finders/groups/user_groups_finder_spec.rb
index 999079468e5..f6df396037c 100644
--- a/spec/finders/groups/user_groups_finder_spec.rb
+++ b/spec/finders/groups/user_groups_finder_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::UserGroupsFinder do
+RSpec.describe Groups::UserGroupsFinder, feature_category: :subgroups do
describe '#execute' do
let_it_be(:user) { create(:user) }
let_it_be(:root_group) { create(:group, name: 'Root group', path: 'root-group') }
@@ -98,6 +98,24 @@ RSpec.describe Groups::UserGroupsFinder do
end
end
+ context 'when permission is :import_projects' do
+ let(:arguments) { { permission_scope: :import_projects } }
+
+ specify do
+ is_expected.to contain_exactly(
+ public_maintainer_group,
+ public_owner_group,
+ private_maintainer_group
+ )
+ end
+
+ it_behaves_like 'user group finder searching by name or path' do
+ let(:keyword_search_expected_groups) do
+ [public_maintainer_group]
+ end
+ end
+ end
+
context 'when permission is :transfer_projects' do
let(:arguments) { { permission_scope: :transfer_projects } }
diff --git a/spec/frontend/import_entities/components/group_dropdown_spec.js b/spec/frontend/import_entities/components/group_dropdown_spec.js
index b44bc33de6f..14f39a35387 100644
--- a/spec/frontend/import_entities/components/group_dropdown_spec.js
+++ b/spec/frontend/import_entities/components/group_dropdown_spec.js
@@ -6,7 +6,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import GroupDropdown from '~/import_entities/components/group_dropdown.vue';
import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
-import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
+import searchNamespacesWhereUserCanImportProjectsQuery from '~/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql';
Vue.use(VueApollo);
@@ -49,7 +49,7 @@ describe('Import entities group dropdown component', () => {
const createComponent = (propsData) => {
const apolloProvider = createMockApollo([
- [searchNamespacesWhereUserCanCreateProjectsQuery, () => SEARCH_NAMESPACES_MOCK],
+ [searchNamespacesWhereUserCanImportProjectsQuery, () => SEARCH_NAMESPACES_MOCK],
]);
namespacesTracker = jest.fn();
diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
index b1aa94cf418..dae5671777c 100644
--- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
@@ -15,7 +15,7 @@ import ImportTable from '~/import_entities/import_groups/components/import_table
import importGroupsMutation from '~/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql';
import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
-import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
+import searchNamespacesWhereUserCanImportProjectsQuery from '~/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql';
import {
AVAILABLE_NAMESPACES,
@@ -74,7 +74,7 @@ describe('import table', () => {
apolloProvider = createMockApollo(
[
[
- searchNamespacesWhereUserCanCreateProjectsQuery,
+ searchNamespacesWhereUserCanImportProjectsQuery,
() => Promise.resolve(availableNamespacesFixture),
],
],
diff --git a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
index a524d9ebdb0..a957e85723f 100644
--- a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
@@ -8,7 +8,7 @@ import ImportGroupDropdown from '~/import_entities/components/group_dropdown.vue
import { STATUSES } from '~/import_entities/constants';
import ImportTargetCell from '~/import_entities/import_groups/components/import_target_cell.vue';
import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
-import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
+import searchNamespacesWhereUserCanImportProjectsQuery from '~/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql';
import {
generateFakeEntry,
@@ -42,7 +42,7 @@ describe('import target cell', () => {
const createComponent = (props) => {
apolloProvider = createMockApollo([
[
- searchNamespacesWhereUserCanCreateProjectsQuery,
+ searchNamespacesWhereUserCanImportProjectsQuery,
() => Promise.resolve(availableNamespacesFixture),
],
]);
diff --git a/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js b/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js
index 53d69b01d97..d4b581c3fcf 100644
--- a/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js
+++ b/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js
@@ -3,6 +3,7 @@ import { mount, shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { s__ } from '~/locale';
import waitForPromises from 'helpers/wait_for_promises';
import JobsTableTabs from '~/jobs/components/table/jobs_table_tabs.vue';
import JobsSkeletonLoader from '~/pages/admin/jobs/components/jobs_skeleton_loader.vue';
@@ -11,16 +12,22 @@ import getCancelableJobsQuery from '~/pages/admin/jobs/components/table/graphql/
import AdminJobsTableApp from '~/pages/admin/jobs/components/table/admin_jobs_table_app.vue';
import CancelJobs from '~/pages/admin/jobs/components/cancel_jobs.vue';
import JobsTable from '~/jobs/components/table/jobs_table.vue';
-
+import { createAlert } from '~/alert';
+import { TEST_HOST } from 'spec/test_constants';
+import JobsFilteredSearch from '~/jobs/components/filtered_search/jobs_filtered_search.vue';
+import * as urlUtils from '~/lib/utils/url_utility';
import {
mockAllJobsResponsePaginated,
mockCancelableJobsCountResponse,
mockAllJobsResponseEmpty,
statuses,
+ mockFailedSearchToken,
} from '../../../../../jobs/mock_data';
Vue.use(VueApollo);
+jest.mock('~/alert');
+
describe('Job table app', () => {
let wrapper;
@@ -36,6 +43,7 @@ describe('Job table app', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findTabs = () => wrapper.findComponent(JobsTableTabs);
const findCancelJobsButton = () => wrapper.findComponent(CancelJobs);
+ const findFilteredSearch = () => wrapper.findComponent(JobsFilteredSearch);
const triggerInfiniteScroll = () =>
wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
@@ -186,4 +194,100 @@ describe('Job table app', () => {
expect(findCancelJobsButton().exists()).toBe(false);
});
});
+
+ describe('filtered search', () => {
+ it('should display filtered search', () => {
+ createComponent();
+
+ expect(findFilteredSearch().exists()).toBe(true);
+ });
+
+ // this test should be updated once BE supports tab and filtered search filtering
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/356210
+ it.each`
+ scope | shouldDisplay
+ ${null} | ${true}
+ ${['FAILED', 'SUCCESS', 'CANCELED']} | ${false}
+ `(
+ 'with tab scope $scope the filtered search displays $shouldDisplay',
+ async ({ scope, shouldDisplay }) => {
+ createComponent();
+
+ await waitForPromises();
+
+ await findTabs().vm.$emit('fetchJobsByStatus', scope);
+
+ expect(findFilteredSearch().exists()).toBe(shouldDisplay);
+ },
+ );
+
+ it('refetches jobs query when filtering', async () => {
+ createComponent();
+
+ jest.spyOn(wrapper.vm.$apollo.queries.jobs, 'refetch').mockImplementation(jest.fn());
+
+ expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0);
+
+ await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]);
+
+ expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(1);
+ });
+
+ it('shows raw text warning when user inputs raw text', async () => {
+ const expectedWarning = {
+ message: s__(
+ 'Jobs|Raw text search is not currently supported for the jobs filtered search feature. Please use the available search tokens.',
+ ),
+ type: 'warning',
+ };
+
+ createComponent();
+
+ jest.spyOn(wrapper.vm.$apollo.queries.jobs, 'refetch').mockImplementation(jest.fn());
+
+ await findFilteredSearch().vm.$emit('filterJobsBySearch', ['raw text']);
+
+ expect(createAlert).toHaveBeenCalledWith(expectedWarning);
+ expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0);
+ });
+
+ it('updates URL query string when filtering jobs by status', async () => {
+ createComponent();
+
+ jest.spyOn(urlUtils, 'updateHistory');
+
+ await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]);
+
+ expect(urlUtils.updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/?statuses=FAILED`,
+ });
+ });
+
+ it('resets query param after clearing tokens', () => {
+ createComponent();
+
+ jest.spyOn(urlUtils, 'updateHistory');
+
+ findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]);
+
+ expect(successHandler).toHaveBeenCalledWith({
+ first: 50,
+ statuses: 'FAILED',
+ });
+ expect(urlUtils.updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/?statuses=FAILED`,
+ });
+
+ findFilteredSearch().vm.$emit('filterJobsBySearch', []);
+
+ expect(urlUtils.updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/`,
+ });
+
+ expect(successHandler).toHaveBeenCalledWith({
+ first: 50,
+ statuses: null,
+ });
+ });
+ });
});
diff --git a/spec/frontend/projects/new/components/app_spec.js b/spec/frontend/projects/new/components/app_spec.js
index 16576523c66..60d8385eb91 100644
--- a/spec/frontend/projects/new/components/app_spec.js
+++ b/spec/frontend/projects/new/components/app_spec.js
@@ -41,6 +41,22 @@ describe('Experimental new project creation app', () => {
).toBe(isCiCdAvailable);
});
+ it.each`
+ canImportProjects | outcome
+ ${false} | ${'do not show Import panel'}
+ ${true} | ${'show Import panel'}
+ `('$outcome when canImportProjects is $canImportProjects', ({ canImportProjects }) => {
+ createComponent({
+ canImportProjects,
+ });
+
+ expect(
+ findNewNamespacePage()
+ .props()
+ .panels.some((p) => p.name === 'import_project'),
+ ).toBe(canImportProjects);
+ });
+
it('creates correct breadcrumbs for top-level projects', () => {
createComponent();
diff --git a/spec/frontend/vue_shared/alert_details/router_spec.js b/spec/frontend/vue_shared/alert_details/router_spec.js
deleted file mode 100644
index e3efc104862..00000000000
--- a/spec/frontend/vue_shared/alert_details/router_spec.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import createRouter from '~/vue_shared/alert_details/router';
-import setWindowLocation from 'helpers/set_window_location_helper';
-
-const BASE_PATH = '/-/alert_management/1/details';
-const EMPTY_HASH = '';
-const NOOP = () => {};
-
-describe('AlertDetails router', () => {
- const originalLocation = window.location.href;
- let router;
-
- beforeEach(() => {
- setWindowLocation(originalLocation);
- router = createRouter(BASE_PATH);
- });
-
- describe('redirects hash route mode URLs to history route mode', () => {
- it.each`
- hashPath | historyPath
- ${'/#/overview'} | ${'/overview'}
- ${'#/overview'} | ${'/overview'}
- ${'/#/'} | ${'/'}
- ${'#/'} | ${'/'}
- ${'/#'} | ${'/'}
- ${'#'} | ${'/'}
- ${'/'} | ${'/'}
- ${'/overview'} | ${'/overview'}
- `('should redirect "$hashPath" to "$historyPath"', ({ hashPath, historyPath }) => {
- router.push(hashPath, NOOP);
-
- expect(window.location.hash).toBe(EMPTY_HASH);
- expect(window.location.pathname).toBe(BASE_PATH + historyPath);
- });
- });
-});
diff --git a/spec/graphql/types/ci/job_type_spec.rb b/spec/graphql/types/ci/job_type_spec.rb
index 7715ccdd075..444e941d7a6 100644
--- a/spec/graphql/types/ci/job_type_spec.rb
+++ b/spec/graphql/types/ci/job_type_spec.rb
@@ -56,6 +56,7 @@ RSpec.describe Types::Ci::JobType, feature_category: :continuous_integration do
canPlayJob
scheduled
trace
+ failure_message
]
expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/support_specs/helpers/packages/npm_spec.rb b/spec/lib/api/helpers/packages/npm_spec.rb
index e1316a10fb1..e1316a10fb1 100644
--- a/spec/support_specs/helpers/packages/npm_spec.rb
+++ b/spec/lib/api/helpers/packages/npm_spec.rb
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 92849efc462..2128e70e432 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -882,46 +882,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
- describe '#has_packages?' do
- let_it_be(:project) { create(:project, :public) }
-
- subject { project.has_packages?(package_type) }
-
- shared_examples 'returning true examples' do
- let!(:package) { create("#{package_type}_package", project: project) }
-
- it { is_expected.to be true }
- end
-
- shared_examples 'returning false examples' do
- it { is_expected.to be false }
- end
-
- context 'with maven packages' do
- it_behaves_like 'returning true examples' do
- let(:package_type) { :maven }
- end
- end
-
- context 'with npm packages' do
- it_behaves_like 'returning true examples' do
- let(:package_type) { :npm }
- end
- end
-
- context 'with conan packages' do
- it_behaves_like 'returning true examples' do
- let(:package_type) { :conan }
- end
- end
-
- context 'with no package type' do
- it_behaves_like 'returning false examples' do
- let(:package_type) { nil }
- end
- end
- end
-
describe '#ci_pipelines' do
let_it_be(:project) { create(:project) }
@@ -7660,48 +7620,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
- describe '#has_packages?' do
- let(:project) { create(:project, :public) }
-
- subject { project.has_packages?(package_type) }
-
- shared_examples 'has_package' do
- context 'package of package_type exists' do
- let!(:package) { create("#{package_type}_package", project: project) }
-
- it { is_expected.to be true }
- end
-
- context 'package of package_type does not exist' do
- it { is_expected.to be false }
- end
- end
-
- context 'with maven packages' do
- it_behaves_like 'has_package' do
- let(:package_type) { :maven }
- end
- end
-
- context 'with npm packages' do
- it_behaves_like 'has_package' do
- let(:package_type) { :npm }
- end
- end
-
- context 'with conan packages' do
- it_behaves_like 'has_package' do
- let(:package_type) { :conan }
- end
- end
-
- context 'calling has_package? with nil' do
- let(:package_type) { nil }
-
- it { is_expected.to be false }
- end
- end
-
describe 'with Debian Distributions' do
subject { create(:project) }
@@ -7822,6 +7740,29 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
+ describe '#has_namespaced_npm_packages?' do
+ let_it_be(:namespace) { create(:namespace, path: 'test') }
+ let_it_be(:project) { create(:project, :public, namespace: namespace) }
+
+ subject { project.has_namespaced_npm_packages? }
+
+ context 'with scope of the namespace path' do
+ let_it_be(:package) { create(:npm_package, project: project, name: "@#{namespace.path}/foo") }
+
+ it { is_expected.to be true }
+ end
+
+ context 'without scope of the namespace path' do
+ let_it_be(:package) { create(:npm_package, project: project, name: "@someotherscope/foo") }
+
+ it { is_expected.to be false }
+ end
+
+ context 'without packages' do
+ it { is_expected.to be false }
+ end
+ end
+
describe '#package_already_taken?' do
let_it_be(:namespace) { create(:namespace, path: 'test') }
let_it_be(:project) { create(:project, :public, namespace: namespace) }
diff --git a/spec/policies/namespaces/user_namespace_policy_spec.rb b/spec/policies/namespaces/user_namespace_policy_spec.rb
index bb821490e30..3488f33f15c 100644
--- a/spec/policies/namespaces/user_namespace_policy_spec.rb
+++ b/spec/policies/namespaces/user_namespace_policy_spec.rb
@@ -2,13 +2,13 @@
require 'spec_helper'
-RSpec.describe Namespaces::UserNamespacePolicy do
+RSpec.describe Namespaces::UserNamespacePolicy, feature_category: :subgroups do
let_it_be(:user) { create(:user) }
let_it_be(:owner) { create(:user) }
let_it_be(:admin) { create(:admin) }
let_it_be(:namespace) { create(:user_namespace, owner: owner) }
- let(:owner_permissions) { [:owner_access, :create_projects, :admin_namespace, :read_namespace, :read_statistics, :transfer_projects, :admin_package, :read_billing, :edit_billing] }
+ let(:owner_permissions) { [:owner_access, :create_projects, :admin_namespace, :read_namespace, :read_statistics, :transfer_projects, :admin_package, :read_billing, :edit_billing, :import_projects] }
subject { described_class.new(current_user, namespace) }
@@ -34,6 +34,7 @@ RSpec.describe Namespaces::UserNamespacePolicy do
it { is_expected.to be_disallowed(:create_projects) }
it { is_expected.to be_disallowed(:transfer_projects) }
+ it { is_expected.to be_disallowed(:import_projects) }
end
context 'bot user' do
@@ -41,6 +42,7 @@ RSpec.describe Namespaces::UserNamespacePolicy do
it { is_expected.to be_disallowed(:create_projects) }
it { is_expected.to be_disallowed(:transfer_projects) }
+ it { is_expected.to be_disallowed(:import_projects) }
end
end
@@ -103,4 +105,26 @@ RSpec.describe Namespaces::UserNamespacePolicy do
it { is_expected.to be_disallowed(:create_projects) }
end
end
+
+ describe 'import projects' do
+ context 'when user can import projects' do
+ let(:current_user) { owner }
+
+ before do
+ allow(current_user).to receive(:can_import_project?).and_return(true)
+ end
+
+ it { is_expected.to be_allowed(:import_projects) }
+ end
+
+ context 'when user cannot create projects' do
+ let(:current_user) { user }
+
+ before do
+ allow(current_user).to receive(:can_import_project?).and_return(false)
+ end
+
+ it { is_expected.to be_disallowed(:import_projects) }
+ end
+ end
end
diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb
index 6bf36a52419..58d19ae2332 100644
--- a/spec/presenters/ci/build_presenter_spec.rb
+++ b/spec/presenters/ci/build_presenter_spec.rb
@@ -174,6 +174,22 @@ RSpec.describe Ci::BuildPresenter do
end
end
+ describe '#failure_message' do
+ let_it_be(:build) { create(:ci_build, :failed, failure_reason: 2) }
+
+ it 'returns a verbose failure message' do
+ expect(subject.failure_message).to eq('There has been an API failure, please try again')
+ end
+
+ context 'when the build has not failed' do
+ let_it_be(:build) { create(:ci_build, :success, failure_reason: 2) }
+
+ it 'does not return any failure message' do
+ expect(subject.failure_message).to be_nil
+ end
+ end
+ end
+
describe '#callout_failure_message' do
let(:build) { create(:ci_build, :failed, :api_failure) }
diff --git a/spec/requests/api/graphql/ci/job_spec.rb b/spec/requests/api/graphql/ci/job_spec.rb
index 8121c5e5c85..960697db239 100644
--- a/spec/requests/api/graphql/ci/job_spec.rb
+++ b/spec/requests/api/graphql/ci/job_spec.rb
@@ -52,7 +52,8 @@ RSpec.describe 'Query.project(fullPath).pipelines.job(id)', feature_category: :c
'duration' => 25,
'kind' => 'BUILD',
'queuedDuration' => 2.0,
- 'status' => job_2.status.upcase
+ 'status' => job_2.status.upcase,
+ 'failureMessage' => job_2.present.failure_message
)
end
diff --git a/spec/requests/import/gitlab_projects_controller_spec.rb b/spec/requests/import/gitlab_projects_controller_spec.rb
index b2c2d306e53..fe3ea9e9c9e 100644
--- a/spec/requests/import/gitlab_projects_controller_spec.rb
+++ b/spec/requests/import/gitlab_projects_controller_spec.rb
@@ -90,4 +90,16 @@ RSpec.describe Import::GitlabProjectsController, feature_category: :importers do
subject { post authorize_import_gitlab_project_path, headers: workhorse_headers }
end
end
+
+ describe 'GET new' do
+ context 'when the user is not allowed to import projects' do
+ let!(:group) { create(:group).tap { |group| group.add_developer(user) } }
+
+ it 'returns 404' do
+ get new_import_gitlab_project_path, params: { namespace_id: group.id }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
end
diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb
index 475cd250e7c..d6eb060ea7e 100644
--- a/spec/services/groups/transfer_service_spec.rb
+++ b/spec/services/groups/transfer_service_spec.rb
@@ -35,10 +35,10 @@ RSpec.describe Groups::TransferService, :sidekiq_inline, feature_category: :subg
end
context 'handling packages' do
- let_it_be(:group) { create(:group, :public) }
- let_it_be(:new_group) { create(:group, :public) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:new_group) { create(:group) }
- let(:project) { create(:project, :public, namespace: group) }
+ let_it_be(:project) { create(:project, namespace: group) }
before do
group.add_owner(user)
@@ -46,46 +46,63 @@ RSpec.describe Groups::TransferService, :sidekiq_inline, feature_category: :subg
end
context 'with an npm package' do
- before do
- create(:npm_package, project: project)
- end
+ let_it_be(:npm_package) { create(:npm_package, project: project, name: "@testscope/test") }
- shared_examples 'transfer not allowed' do
- it 'does not allow transfer when there is a root namespace change' do
+ shared_examples 'transfer allowed' do
+ it 'allows transfer' do
transfer_service.execute(new_group)
- expect(transfer_service.error).to eq('Transfer failed: Group contains projects with NPM packages.')
- expect(group.parent).not_to eq(new_group)
+ expect(transfer_service.error).to be nil
+ expect(group.parent).to eq(new_group)
end
end
- it_behaves_like 'transfer not allowed'
+ it_behaves_like 'transfer allowed'
context 'with a project within subgroup' do
let_it_be(:root_group) { create(:group) }
let_it_be(:group) { create(:group, parent: root_group) }
+ let_it_be(:project) { create(:project, namespace: group) }
before do
root_group.add_owner(user)
end
- it_behaves_like 'transfer not allowed'
+ it_behaves_like 'transfer allowed'
context 'without a root namespace change' do
- let(:new_group) { create(:group, parent: root_group) }
+ let_it_be(:new_group) { create(:group, parent: root_group) }
+
+ it_behaves_like 'transfer allowed'
+ end
+
+ context 'with namespaced packages present' do
+ let_it_be(:package) { create(:npm_package, project: project, name: "@#{project.root_namespace.path}/test") }
- it 'allows transfer' do
+ it 'does not allow transfer' do
transfer_service.execute(new_group)
- expect(transfer_service.error).to be nil
- expect(group.parent).to eq(new_group)
+ expect(transfer_service.error).to eq('Transfer failed: Group contains projects with NPM packages scoped to the current root level group.')
+ expect(group.parent).not_to eq(new_group)
+ end
+
+ context 'namespaced package is pending destruction' do
+ let!(:group) { create(:group) }
+
+ before do
+ package.pending_destruction!
+ end
+
+ it_behaves_like 'transfer allowed'
end
end
context 'when transferring a group into a root group' do
- let(:new_group) { nil }
+ let_it_be(:root_group) { create(:group) }
+ let_it_be(:group) { create(:group, parent: root_group) }
+ let_it_be(:new_group) { nil }
- it_behaves_like 'transfer not allowed'
+ it_behaves_like 'transfer allowed'
end
end
end
diff --git a/spec/services/import/bitbucket_server_service_spec.rb b/spec/services/import/bitbucket_server_service_spec.rb
index aea6c45b3a8..ca554fb01c3 100644
--- a/spec/services/import/bitbucket_server_service_spec.rb
+++ b/spec/services/import/bitbucket_server_service_spec.rb
@@ -93,7 +93,7 @@ RSpec.describe Import::BitbucketServerService, feature_category: :importers do
result = subject.execute(credentials)
expect(result).to include(
- message: "You don't have permissions to create this project",
+ message: "You don't have permissions to import this project",
status: :error,
http_status: :unauthorized
)
diff --git a/spec/services/import/fogbugz_service_spec.rb b/spec/services/import/fogbugz_service_spec.rb
index 6953213add7..ad02dc31da1 100644
--- a/spec/services/import/fogbugz_service_spec.rb
+++ b/spec/services/import/fogbugz_service_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe Import::FogbugzService, feature_category: :importers do
result = subject.execute(credentials)
expect(result).to include(
- message: "You don't have permissions to create this project",
+ message: "You don't have permissions to import this project",
status: :error,
http_status: :unauthorized
)
diff --git a/spec/services/import/github_service_spec.rb b/spec/services/import/github_service_spec.rb
index 5d762568a62..a8928fb5c09 100644
--- a/spec/services/import/github_service_spec.rb
+++ b/spec/services/import/github_service_spec.rb
@@ -291,7 +291,7 @@ RSpec.describe Import::GithubService, feature_category: :importers do
{
status: :error,
http_status: :unprocessable_entity,
- message: 'This namespace has already been taken. Choose a different one.'
+ message: 'You are not allowed to import projects in this namespace.'
}
end
end
diff --git a/spec/services/packages/npm/create_package_service_spec.rb b/spec/services/packages/npm/create_package_service_spec.rb
index d21b11f8ecb..638004f2520 100644
--- a/spec/services/packages/npm/create_package_service_spec.rb
+++ b/spec/services/packages/npm/create_package_service_spec.rb
@@ -61,17 +61,90 @@ RSpec.describe Packages::Npm::CreatePackageService, feature_category: :package_r
end
end
- context 'with a too large metadata structure' do
- before do
- params[:versions][version][:test] = 'test' * 10000
+ context 'when the npm metadatum creation results in a size error' do
+ shared_examples 'a package json structure size too large error' do
+ it 'does not create the package' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
+ instance_of(ActiveRecord::RecordInvalid),
+ field_sizes: expected_field_sizes
+ )
+
+ expect { subject }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Package json structure is too large')
+ .and not_change { Packages::Package.count }
+ .and not_change { Packages::Package.npm.count }
+ .and not_change { Packages::Tag.count }
+ .and not_change { Packages::Npm::Metadatum.count }
+ end
+ end
+
+ context 'when some of the field sizes are above the error tracking size' do
+ let(:package_json) do
+ params[:versions][version].except(*::Packages::Npm::CreatePackageService::PACKAGE_JSON_NOT_ALLOWED_FIELDS)
+ end
+
+ # Only the fields that exceed the field size limit should be passed to error tracking
+ let(:expected_field_sizes) do
+ {
+ 'test' => ('test' * 10000).size,
+ 'field2' => ('a' * (::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING + 1)).size
+ }
+ end
+
+ before do
+ params[:versions][version][:test] = 'test' * 10000
+ params[:versions][version][:field1] =
+ 'a' * (::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING - 1)
+ params[:versions][version][:field2] =
+ 'a' * (::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING + 1)
+ end
+
+ it_behaves_like 'a package json structure size too large error'
+ end
+
+ context 'when all of the field sizes are below the error tracking size' do
+ let(:package_json) do
+ params[:versions][version].except(*::Packages::Npm::CreatePackageService::PACKAGE_JSON_NOT_ALLOWED_FIELDS)
+ end
+
+ let(:expected_size) { ('a' * (::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING - 1)).size }
+ # Only the five largest fields should be passed to error tracking
+ let(:expected_field_sizes) do
+ {
+ 'field1' => expected_size,
+ 'field2' => expected_size,
+ 'field3' => expected_size,
+ 'field4' => expected_size,
+ 'field5' => expected_size
+ }
+ end
+
+ before do
+ 5.times do |i|
+ params[:versions][version]["field#{i + 1}"] =
+ 'a' * (::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING - 1)
+ end
+ end
+
+ it_behaves_like 'a package json structure size too large error'
end
+ end
+
+ context 'when the npm metadatum creation results in a different error' do
+ it 'does not track the error' do
+ error_message = 'boom'
+ invalid_npm_metadatum_error = ActiveRecord::RecordInvalid.new(
+ build(:npm_metadatum).tap do |metadatum|
+ metadatum.errors.add(:base, error_message)
+ end
+ )
+
+ allow_next_instance_of(::Packages::Package) do |package|
+ allow(package).to receive(:create_npm_metadatum!).and_raise(invalid_npm_metadatum_error)
+ end
+
+ expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
- it 'does not create the package' do
- expect { subject }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Package json structure is too large')
- .and not_change { Packages::Package.count }
- .and not_change { Packages::Package.npm.count }
- .and not_change { Packages::Tag.count }
- .and not_change { Packages::Npm::Metadatum.count }
+ expect { subject }.to raise_error(ActiveRecord::RecordInvalid, /#{error_message}/)
end
end
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 495e2277d43..35b715d82ee 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -254,6 +254,23 @@ RSpec.describe Projects::CreateService, '#execute', feature_category: :projects
end
it_behaves_like 'has sync-ed traversal_ids'
+
+ context 'when project is an import' do
+ context 'when user is not allowed to import projects' do
+ let(:group) do
+ create(:group).tap do |group|
+ group.add_developer(user)
+ end
+ end
+
+ it 'does not create the project' do
+ project = create_project(user, opts.merge!(namespace_id: group.id, import_type: 'gitlab_project'))
+
+ expect(project).not_to be_persisted
+ expect(project.errors.messages[:user].first).to eq('is not allowed to import projects')
+ end
+ end
+ end
end
context 'group sharing', :sidekiq_inline do
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 92ed5ef3f0a..2704458ca4d 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -20,12 +20,32 @@ RSpec.describe Projects::TransferService, feature_category: :projects do
subject(:transfer_service) { described_class.new(project, user) }
- let!(:package) { create(:npm_package, project: project) }
+ let!(:package) { create(:npm_package, project: project, name: "@testscope/test") }
context 'with a root namespace change' do
+ it 'allow the transfer' do
+ expect(transfer_service.execute(group)).to be true
+ expect(project.errors[:new_namespace]).to be_empty
+ end
+ end
+
+ context 'with pending destruction package' do
+ before do
+ package.pending_destruction!
+ end
+
+ it 'allow the transfer' do
+ expect(transfer_service.execute(group)).to be true
+ expect(project.errors[:new_namespace]).to be_empty
+ end
+ end
+
+ context 'with namespaced packages present' do
+ let!(:package) { create(:npm_package, project: project, name: "@#{project.root_namespace.path}/test") }
+
it 'does not allow the transfer' do
expect(transfer_service.execute(group)).to be false
- expect(project.errors[:new_namespace]).to include("Root namespace can't be updated if project has NPM packages")
+ expect(project.errors[:new_namespace]).to include("Root namespace can't be updated if the project has NPM packages scoped to the current root level namespace.")
end
end
@@ -39,7 +59,7 @@ RSpec.describe Projects::TransferService, feature_category: :projects do
other_group.add_owner(user)
end
- it 'does allow the transfer' do
+ it 'allow the transfer' do
expect(transfer_service.execute(other_group)).to be true
expect(project.errors[:new_namespace]).to be_empty
end
diff --git a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
index de38d1ff9f8..af1843bae28 100644
--- a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
@@ -138,6 +138,19 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do
.not_to exceed_all_query_limit(control_count)
end
+ context 'when user is not allowed to import projects' do
+ let(:user) { create(:user) }
+ let!(:group) { create(:group).tap { |group| group.add_developer(user) } }
+
+ it 'returns 404' do
+ expect(stub_client(repos: [], orgs: [])).to receive(:repos)
+
+ get :status, params: { namespace_id: group.id }, format: :html
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
context 'when filtering' do
let(:repo_2) { repo_fake.new(login: 'emacs', full_name: 'asd/emacs', name: 'emacs', owner: { login: 'owner' }) }
let(:project) { create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'example/repo') }
diff --git a/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb b/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb
index 44baadaaade..e94f063399d 100644
--- a/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb
@@ -19,4 +19,26 @@ RSpec.shared_examples 'import controller status' do
expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id)
expect(json_response.dig("provider_repos", 0, "id")).to eq(repo_id)
end
+
+ context 'when format is html' do
+ context 'when namespace_id is present' do
+ let!(:developer_group) { create(:group).tap { |g| g.add_developer(user) } }
+
+ context 'when user cannot import projects' do
+ it 'returns 404' do
+ get :status, params: { namespace_id: developer_group.id }, format: :html
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when user can import projects' do
+ it 'returns 200' do
+ get :status, params: { namespace_id: group.id }, format: :html
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+ end
end