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--app/assets/javascripts/ide/components/commit_sidebar/actions.vue2
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue34
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue2
-rw-r--r--app/assets/javascripts/ide/components/nav_dropdown.vue10
-rw-r--r--app/assets/javascripts/ide/components/nav_dropdown_button.vue11
-rw-r--r--app/assets/javascripts/ide/components/nav_form.vue10
-rw-r--r--app/assets/javascripts/ide/constants.js3
-rw-r--r--app/assets/javascripts/ide/queries/getUserPermissions.query.graphql8
-rw-r--r--app/assets/javascripts/ide/services/gql.js8
-rw-r--r--app/assets/javascripts/ide/services/index.js23
-rw-r--r--app/assets/javascripts/ide/stores/actions/merge_request.js14
-rw-r--r--app/assets/javascripts/ide/stores/getters.js16
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/getters.js6
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/index.js4
-rw-r--r--app/assets/javascripts/repository/components/table/index.vue7
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue8
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss2
-rw-r--r--app/controllers/admin/application_settings_controller.rb24
-rw-r--r--app/services/projects/lsif_data_service.rb17
-rw-r--r--app/views/admin/application_settings/metrics_and_profiling.html.haml3
-rw-r--r--app/views/projects/_home_panel.html.haml4
-rw-r--r--app/views/projects/tree/_tree_content.html.haml2
-rw-r--r--app/views/projects/tree/_tree_header.html.haml17
-rw-r--r--changelogs/unreleased/35428-web-ide-button-missing-from-project-pages.yml5
-rw-r--r--changelogs/unreleased/dblessing_disable_enforced_sso_plan_expires.yml5
-rw-r--r--changelogs/unreleased/georgekoltsov-fix-group-import-visibility-level.yml6
-rw-r--r--changelogs/unreleased/refactoring-entities-file-27.yml5
-rw-r--r--changelogs/unreleased/refactoring-entities-file-29.yml5
-rw-r--r--changelogs/unreleased/remove-self-monitoring-feature-flag.yml5
-rw-r--r--doc/development/go_guide/index.md1
-rw-r--r--lib/api/entities.rb44
-rw-r--r--lib/api/entities/application.rb13
-rw-r--r--lib/api/entities/application_with_secret.rb10
-rw-r--r--lib/api/entities/blob.rb20
-rw-r--r--lib/api/entities/custom_attribute.rb10
-rw-r--r--lib/api/entities/pages_domain_certificate_expiration.rb10
-rw-r--r--lib/api/entities/user_agent_detail.rb11
-rw-r--r--lib/api/group_import.rb15
-rw-r--r--lib/gitlab/import_export/group_import_export.yml1
-rw-r--r--lib/gitlab/import_export/group_tree_restorer.rb13
-rw-r--r--locale/gitlab.pot6
-rw-r--r--qa/qa/page/project/show.rb35
-rw-r--r--qa/qa/resource/api_fabricator.rb6
-rw-r--r--qa/qa/resource/fork.rb52
-rw-r--r--qa/qa/resource/merge_request_from_fork.rb2
-rw-r--r--qa/qa/resource/project.rb33
-rw-r--r--qa/qa/runtime/env.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/move_project_create_fork_spec.rb53
-rw-r--r--spec/features/projects/show/user_sees_collaboration_links_spec.rb106
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/internal/group.json166
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/private/group.json166
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/public/group.json166
-rw-r--r--spec/fixtures/lsif.json.gzbin739 -> 759 bytes
-rw-r--r--spec/frontend/ide/mock_data.js1
-rw-r--r--spec/frontend/ide/services/index_spec.js31
-rw-r--r--spec/frontend/ide/stores/getters_spec.js36
-rw-r--r--spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap2
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/new_merge_request_option_spec.js53
-rw-r--r--spec/javascripts/ide/components/nav_dropdown_button_spec.js104
-rw-r--r--spec/javascripts/ide/components/nav_dropdown_spec.js30
-rw-r--r--spec/javascripts/ide/stores/actions/merge_request_spec.js18
-rw-r--r--spec/javascripts/ide/stores/modules/commit/actions_spec.js5
-rw-r--r--spec/lib/gitlab/import_export/group_tree_restorer_spec.rb27
-rw-r--r--spec/lib/gitlab/import_export/group_tree_saver_spec.rb2
-rw-r--r--spec/requests/api/group_import_spec.rb36
-rw-r--r--spec/requests/api/lsif_data_spec.rb6
-rw-r--r--spec/requests/api/merge_requests_spec.rb102
-rw-r--r--spec/requests/self_monitoring_project_spec.rb24
-rw-r--r--spec/services/projects/lsif_data_service_spec.rb25
-rw-r--r--spec/support/helpers/api_helpers.rb7
-rw-r--r--spec/support/shared_examples/requests/self_monitoring_shared_examples.rb18
-rw-r--r--spec/views/projects/tree/_tree_header.html.haml_spec.rb13
74 files changed, 1429 insertions, 324 deletions
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
index d9cd4f3acf1..2581c3e9928 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
@@ -70,7 +70,7 @@ export default {
:title="$options.currentBranchPermissionsTooltip"
>
<span
- class="ide-radio-label"
+ class="ide-option-label"
data-qa-selector="commit_to_current_branch_radio"
v-html="commitToCurrentBranchText"
></span>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue b/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue
index daa44a42765..0812599c25c 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue
@@ -1,16 +1,27 @@
<script>
import { createNamespacedHelpers } from 'vuex';
+import { GlTooltipDirective } from '@gitlab/ui';
+import { s__ } from '~/locale';
-const {
- mapState: mapCommitState,
- mapActions: mapCommitActions,
- mapGetters: mapCommitGetters,
-} = createNamespacedHelpers('commit');
+const { mapActions: mapCommitActions, mapGetters: mapCommitGetters } = createNamespacedHelpers(
+ 'commit',
+);
export default {
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
computed: {
- ...mapCommitState(['shouldCreateMR']),
- ...mapCommitGetters(['shouldHideNewMrOption']),
+ ...mapCommitGetters(['shouldHideNewMrOption', 'shouldDisableNewMrOption', 'shouldCreateMR']),
+ tooltipText() {
+ if (this.shouldDisableNewMrOption) {
+ return s__(
+ 'IDE|This option is disabled because you are not allowed to create merge requests in this project.',
+ );
+ }
+
+ return '';
+ },
},
methods: {
...mapCommitActions(['toggleShouldCreateMR']),
@@ -21,14 +32,19 @@ export default {
<template>
<fieldset v-if="!shouldHideNewMrOption">
<hr class="my-2" />
- <label class="mb-0 js-ide-commit-new-mr">
+ <label
+ v-gl-tooltip="tooltipText"
+ class="mb-0 js-ide-commit-new-mr"
+ :class="{ 'is-disabled': shouldDisableNewMrOption }"
+ >
<input
+ :disabled="shouldDisableNewMrOption"
:checked="shouldCreateMR"
type="checkbox"
data-qa-selector="start_new_mr_checkbox"
@change="toggleShouldCreateMR"
/>
- <span class="prepend-left-10">
+ <span class="prepend-left-10 ide-option-label">
{{ __('Start a new merge request') }}
</span>
</label>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
index 07073f5f879..a9591805261 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
@@ -67,7 +67,7 @@ export default {
@change="updateCommitAction($event.target.value)"
/>
<span class="prepend-left-10">
- <span v-if="label" class="ide-radio-label"> {{ label }} </span> <slot v-else></slot>
+ <span v-if="label" class="ide-option-label"> {{ label }} </span> <slot v-else></slot>
</span>
</label>
<div v-if="commitAction === value && showInput" class="ide-commit-new-branch">
diff --git a/app/assets/javascripts/ide/components/nav_dropdown.vue b/app/assets/javascripts/ide/components/nav_dropdown.vue
index 2e290de0943..2307efd1d24 100644
--- a/app/assets/javascripts/ide/components/nav_dropdown.vue
+++ b/app/assets/javascripts/ide/components/nav_dropdown.vue
@@ -1,5 +1,6 @@
<script>
import $ from 'jquery';
+import { mapGetters } from 'vuex';
import NavForm from './nav_form.vue';
import NavDropdownButton from './nav_dropdown_button.vue';
@@ -13,6 +14,9 @@ export default {
isVisibleDropdown: false,
};
},
+ computed: {
+ ...mapGetters(['canReadMergeRequests']),
+ },
mounted() {
this.addDropdownListeners();
},
@@ -42,7 +46,9 @@ export default {
<template>
<div ref="dropdown" class="btn-group ide-nav-dropdown dropdown">
- <nav-dropdown-button />
- <div class="dropdown-menu dropdown-menu-left p-0"><nav-form v-if="isVisibleDropdown" /></div>
+ <nav-dropdown-button :show-merge-requests="canReadMergeRequests" />
+ <div class="dropdown-menu dropdown-menu-left p-0">
+ <nav-form v-if="isVisibleDropdown" :show-merge-requests="canReadMergeRequests" />
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/nav_dropdown_button.vue b/app/assets/javascripts/ide/components/nav_dropdown_button.vue
index f1d44443125..4cd320d5d66 100644
--- a/app/assets/javascripts/ide/components/nav_dropdown_button.vue
+++ b/app/assets/javascripts/ide/components/nav_dropdown_button.vue
@@ -10,6 +10,13 @@ export default {
Icon,
DropdownButton,
},
+ props: {
+ showMergeRequests: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
computed: {
...mapState(['currentBranchId', 'currentMergeRequestId']),
mergeRequestLabel() {
@@ -25,10 +32,10 @@ export default {
<template>
<dropdown-button>
<span class="row">
- <span class="col-7 text-truncate">
+ <span class="col-auto text-truncate" :class="{ 'col-7': showMergeRequests }">
<icon :size="16" :aria-label="__('Current Branch')" name="branch" /> {{ branchLabel }}
</span>
- <span class="col-5 pl-0 text-truncate">
+ <span v-if="showMergeRequests" class="col-5 pl-0 text-truncate">
<icon :size="16" :aria-label="__('Merge Request')" name="merge-request" />
{{ mergeRequestLabel }}
</span>
diff --git a/app/assets/javascripts/ide/components/nav_form.vue b/app/assets/javascripts/ide/components/nav_form.vue
index 2ccc84ea5d5..195504a6861 100644
--- a/app/assets/javascripts/ide/components/nav_form.vue
+++ b/app/assets/javascripts/ide/components/nav_form.vue
@@ -11,12 +11,19 @@ export default {
BranchesSearchList,
MergeRequestSearchList,
},
+ props: {
+ showMergeRequests: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
};
</script>
<template>
<div class="ide-nav-form p-0">
- <tabs stop-propagation>
+ <tabs v-if="showMergeRequests" stop-propagation>
<tab active>
<template slot="title">
{{ __('Branches') }}
@@ -30,5 +37,6 @@ export default {
<merge-request-search-list />
</tab>
</tabs>
+ <branches-search-list v-else />
</div>
</template>
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index 673ac1bfa9a..54d3e79411f 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -8,6 +8,9 @@ export const MAX_BODY_LENGTH = 72;
export const FILE_VIEW_MODE_EDITOR = 'editor';
export const FILE_VIEW_MODE_PREVIEW = 'preview';
+export const PERMISSION_CREATE_MR = 'createMergeRequestIn';
+export const PERMISSION_READ_MR = 'readMergeRequest';
+
export const activityBarViews = {
edit: 'ide-tree',
commit: 'commit-section',
diff --git a/app/assets/javascripts/ide/queries/getUserPermissions.query.graphql b/app/assets/javascripts/ide/queries/getUserPermissions.query.graphql
new file mode 100644
index 00000000000..48f63995f44
--- /dev/null
+++ b/app/assets/javascripts/ide/queries/getUserPermissions.query.graphql
@@ -0,0 +1,8 @@
+query getUserPermissions($projectPath: ID!) {
+ project(fullPath: $projectPath) {
+ userPermissions {
+ createMergeRequestIn,
+ readMergeRequest
+ }
+ }
+}
diff --git a/app/assets/javascripts/ide/services/gql.js b/app/assets/javascripts/ide/services/gql.js
new file mode 100644
index 00000000000..8a7f27328ba
--- /dev/null
+++ b/app/assets/javascripts/ide/services/gql.js
@@ -0,0 +1,8 @@
+import createGqClient, { fetchPolicies } from '~/lib/graphql';
+
+export default createGqClient(
+ {},
+ {
+ fetchPolicy: fetchPolicies.NO_CACHE,
+ },
+);
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index a5134c64705..84a2b2bd58e 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -1,6 +1,18 @@
import axios from '~/lib/utils/axios_utils';
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import Api from '~/api';
+import getUserPermissions from '../queries/getUserPermissions.query.graphql';
+import gqClient from './gql';
+
+const fetchApiProjectData = projectPath => Api.project(projectPath).then(({ data }) => data);
+
+const fetchGqlProjectData = projectPath =>
+ gqClient
+ .query({
+ query: getUserPermissions,
+ variables: { projectPath },
+ })
+ .then(({ data }) => data.project);
export default {
getFileData(endpoint) {
@@ -47,7 +59,16 @@ export default {
.then(({ data }) => data);
},
getProjectData(namespace, project) {
- return Api.project(`${namespace}/${project}`);
+ const projectPath = `${namespace}/${project}`;
+
+ return Promise.all([fetchApiProjectData(projectPath), fetchGqlProjectData(projectPath)]).then(
+ ([apiProjectData, gqlProjectData]) => ({
+ data: {
+ ...apiProjectData,
+ ...gqlProjectData,
+ },
+ }),
+ );
},
getProjectMergeRequests(projectId, params = {}) {
return Api.projectMergeRequests(projectId, params);
diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js
index aa44067edf8..9e9c6fc42b3 100644
--- a/app/assets/javascripts/ide/stores/actions/merge_request.js
+++ b/app/assets/javascripts/ide/stores/actions/merge_request.js
@@ -2,10 +2,17 @@ import flash from '~/flash';
import { __ } from '~/locale';
import service from '../../services';
import * as types from '../mutation_types';
-import { activityBarViews } from '../../constants';
+import { activityBarViews, PERMISSION_READ_MR } from '../../constants';
-export const getMergeRequestsForBranch = ({ commit, state }, { projectId, branchId } = {}) =>
- service
+export const getMergeRequestsForBranch = (
+ { commit, state, getters },
+ { projectId, branchId } = {},
+) => {
+ if (!getters.findProjectPermissions(projectId)[PERMISSION_READ_MR]) {
+ return Promise.resolve();
+ }
+
+ return service
.getProjectMergeRequests(`${projectId}`, {
source_branch: branchId,
source_project_id: state.projects[projectId].id,
@@ -36,6 +43,7 @@ export const getMergeRequestsForBranch = ({ commit, state }, { projectId, branch
);
throw e;
});
+};
export const getMergeRequestData = (
{ commit, dispatch, state },
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index 2fc574cd343..257062d118c 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -1,5 +1,10 @@
import { getChangesCountForFiles, filePathMatches } from './utils';
-import { activityBarViews, packageJsonPath } from '../constants';
+import {
+ activityBarViews,
+ packageJsonPath,
+ PERMISSION_READ_MR,
+ PERMISSION_CREATE_MR,
+} from '../constants';
export const activeFile = state => state.openFiles.find(file => file.active) || null;
@@ -141,5 +146,14 @@ export const getDiffInfo = (state, getters) => path => {
};
};
+export const findProjectPermissions = (state, getters) => projectId =>
+ getters.findProject(projectId)?.userPermissions || {};
+
+export const canReadMergeRequests = (state, getters) =>
+ Boolean(getters.findProjectPermissions(state.currentProjectId)[PERMISSION_READ_MR]);
+
+export const canCreateMergeRequests = (state, getters) =>
+ Boolean(getters.findProjectPermissions(state.currentProjectId)[PERMISSION_CREATE_MR]);
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index 0740e0523a9..3be350db3da 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -158,7 +158,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true });
}, 5000);
- if (state.shouldCreateMR) {
+ if (getters.shouldCreateMR) {
const { currentProject } = rootGetters;
const targetBranch = getters.isCreatingNewBranch
? rootState.currentBranchId
diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js
index de289e27199..e421d44b6de 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js
@@ -54,5 +54,11 @@ export const shouldHideNewMrOption = (_state, getters, _rootState, rootGetters)
(!rootGetters.hasMergeRequest && rootGetters.isOnDefaultBranch)) &&
rootGetters.canPushToBranch;
+export const shouldDisableNewMrOption = (state, getters, rootState, rootGetters) =>
+ !rootGetters.canCreateMergeRequests;
+
+export const shouldCreateMR = (state, getters) =>
+ state.shouldCreateMR && !getters.shouldDisableNewMrOption;
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/pages/admin/application_settings/index.js b/app/assets/javascripts/pages/admin/application_settings/index.js
index 089dedd14cb..78b7e29ae53 100644
--- a/app/assets/javascripts/pages/admin/application_settings/index.js
+++ b/app/assets/javascripts/pages/admin/application_settings/index.js
@@ -3,9 +3,7 @@ import projectSelect from '~/project_select';
import selfMonitor from '~/self_monitor';
document.addEventListener('DOMContentLoaded', () => {
- if (gon.features && gon.features.selfMonitoringProject) {
- selfMonitor();
- }
+ selfMonitor();
// Initialize expandable settings panels
initSettingsPanels();
projectSelect();
diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue
index 29a3340b83d..2ba170998e8 100644
--- a/app/assets/javascripts/repository/components/table/index.vue
+++ b/app/assets/javascripts/repository/components/table/index.vue
@@ -71,7 +71,12 @@ export default {
<template>
<div class="tree-content-holder">
<div class="table-holder bordered-box">
- <table :aria-label="tableCaption" class="table tree-table qa-file-tree" aria-live="polite">
+ <table
+ :aria-label="tableCaption"
+ class="table tree-table"
+ aria-live="polite"
+ data-qa-selector="file_tree_table"
+ >
<table-header v-once />
<tbody>
<parent-row
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index 8703796b116..c905c39bbba 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -139,7 +139,13 @@ export default {
class="d-inline-block align-text-bottom fa-fw"
/>
<i v-else :aria-label="type" role="img" :class="iconName" class="fa fa-fw"></i>
- <component :is="linkComponent" :to="routerLinkTo" :href="url" class="str-truncated">
+ <component
+ :is="linkComponent"
+ :to="routerLinkTo"
+ :href="url"
+ class="str-truncated"
+ data-qa-selector="file_name_link"
+ >
{{ fullPath }}
</component>
<!-- eslint-disable-next-line @gitlab/vue-i18n/no-bare-strings -->
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 58279bba4ca..990aca5f0c5 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -688,7 +688,7 @@ $ide-commit-header-height: 48px;
font-weight: normal;
&.is-disabled {
- .ide-radio-label {
+ .ide-option-label {
text-decoration: line-through;
}
}
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 00771aff26c..8a583e16c0b 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -11,16 +11,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
before_action :set_application_setting
before_action :whitelist_query_limiting, only: [:usage_data]
- before_action :validate_self_monitoring_feature_flag_enabled, only: [
- :create_self_monitoring_project,
- :status_create_self_monitoring_project,
- :delete_self_monitoring_project,
- :status_delete_self_monitoring_project
- ]
-
- before_action do
- push_frontend_feature_flag(:self_monitoring_project)
- end
VALID_SETTING_PANELS = %w(general integrations repository
ci_cd reporting metrics_and_profiling
@@ -163,10 +153,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
private
- def validate_self_monitoring_feature_flag_enabled
- self_monitoring_project_not_implemented unless Feature.enabled?(:self_monitoring_project)
- end
-
def self_monitoring_data
{
project_id: @application_setting.self_monitoring_project_id,
@@ -174,16 +160,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
}
end
- def self_monitoring_project_not_implemented
- render(
- status: :not_implemented,
- json: {
- message: _('Self-monitoring is not enabled on this GitLab server, contact your administrator.'),
- documentation_url: help_page_path('administration/monitoring/gitlab_self_monitoring_project/index')
- }
- )
- end
-
def set_application_setting
@application_setting = ApplicationSetting.current_without_cache
end
diff --git a/app/services/projects/lsif_data_service.rb b/app/services/projects/lsif_data_service.rb
index 1282a0736e7..971885b680e 100644
--- a/app/services/projects/lsif_data_service.rb
+++ b/app/services/projects/lsif_data_service.rb
@@ -3,7 +3,7 @@
module Projects
class LsifDataService
attr_reader :file, :project, :path, :commit_id,
- :docs, :doc_ranges, :ranges, :def_refs
+ :docs, :doc_ranges, :ranges, :def_refs, :hover_refs
CACHE_EXPIRE_IN = 1.hour
@@ -26,7 +26,8 @@ module Projects
end_line: line_data.last,
start_char: column_data.first,
end_char: column_data.last,
- definition_url: definition_url_for(def_refs[ref_id])
+ definition_url: definition_url_for(def_refs[ref_id]),
+ hover: highlighted_hover(hover_refs[ref_id])
}
end
end
@@ -54,6 +55,7 @@ module Projects
@doc_ranges = data['doc_ranges']
@ranges = data['ranges']
@def_refs = data['def_refs']
+ @hover_refs = data['hover_refs']
end
def doc_id
@@ -86,5 +88,16 @@ module Projects
Gitlab::Routing.url_helpers.project_blob_path(project, definition_ref_path, anchor: line_anchor)
end
+
+ def highlighted_hover(hovers)
+ hovers&.map do |hover|
+ # Documentation for a method which is added as comments on top of the method
+ # is stored as a raw string value in LSIF file
+ next { value: hover } unless hover.is_a?(Hash)
+
+ value = Gitlab::Highlight.highlight(nil, hover['value'], language: hover['language'])
+ { language: hover['language'], value: value }
+ end
+ end
end
end
diff --git a/app/views/admin/application_settings/metrics_and_profiling.html.haml b/app/views/admin/application_settings/metrics_and_profiling.html.haml
index ff40d7da892..0b747082de0 100644
--- a/app/views/admin/application_settings/metrics_and_profiling.html.haml
+++ b/app/views/admin/application_settings/metrics_and_profiling.html.haml
@@ -47,8 +47,7 @@
.settings-content
= render 'performance_bar'
-- if Feature.enabled?(:self_monitoring_project)
- .js-self-monitoring-settings{ data: self_monitoring_project_data }
+.js-self-monitoring-settings{ data: self_monitoring_project_data }
%section.settings.as-usage.no-animate#js-usage-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header#usage-statistics
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 7796db5ba63..d9887cb470a 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -10,7 +10,7 @@
= project_icon(@project, alt: @project.name, class: 'avatar avatar-tile s64', width: 64, height: 64)
.d-flex.flex-column.flex-wrap.align-items-baseline
.d-inline-flex.align-items-baseline
- %h1.home-panel-title.prepend-top-8.append-bottom-5.qa-project-name
+ %h1.home-panel-title.prepend-top-8.append-bottom-5{ data: { qa_selector: 'project_name_content' } }
= @project.name
%span.visibility-icon.text-secondary.prepend-left-4.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) }
= visibility_level_icon(@project.visibility_level, fw: false, options: {class: 'icon'})
@@ -70,7 +70,7 @@
- source = visible_fork_source(@project)
- if source
#{ s_('ForkedFromProjectPath|Forked from') }
- = link_to source.full_name, project_path(source)
+ = link_to source.full_name, project_path(source), data: { qa_selector: 'forked_from_link' }
- else
= s_('ForkedFromProjectPath|Forked from an inaccessible project')
diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml
index cb459b031fc..c65420d537b 100644
--- a/app/views/projects/tree/_tree_content.html.haml
+++ b/app/views/projects/tree/_tree_content.html.haml
@@ -1,6 +1,6 @@
.tree-content-holder.js-tree-content{ data: tree_content_data(@logs_path, @project, @path) }
.table-holder.bordered-box
- %table.table#tree-slider{ class: "table_#{@hex_path} tree-table qa-file-tree" }
+ %table.table#tree-slider{ class: "table_#{@hex_path} tree-table" }
%thead
%tr
%th= s_('ProjectFileTree|Name')
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 849d9d7e87c..4d3c24aee6b 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -84,17 +84,16 @@
= render 'projects/find_file_link'
- - if can_create_mr_from_fork
- - if can_collaborate || current_user&.already_forked?(@project)
- - if vue_file_list_enabled?
- #js-tree-web-ide-link.d-inline-block
- - else
- = link_to ide_edit_path(@project, @ref, @path), class: 'btn btn-default qa-web-ide-button' do
- = _('Web IDE')
+ - if can_collaborate || current_user&.already_forked?(@project)
+ - if vue_file_list_enabled?
+ #js-tree-web-ide-link.d-inline-block
- else
- = link_to '#modal-confirm-fork', class: 'btn btn-default qa-web-ide-button', data: { target: '#modal-confirm-fork', toggle: 'modal'} do
+ = link_to ide_edit_path(@project, @ref, @path), class: 'btn btn-default qa-web-ide-button' do
= _('Web IDE')
- = render 'shared/confirm_fork_modal', fork_path: ide_fork_and_edit_path(@project, @ref, @path)
+ - elsif can_create_mr_from_fork
+ = link_to '#modal-confirm-fork', class: 'btn btn-default qa-web-ide-button', data: { target: '#modal-confirm-fork', toggle: 'modal'} do
+ = _('Web IDE')
+ = render 'shared/confirm_fork_modal', fork_path: ide_fork_and_edit_path(@project, @ref, @path)
- if show_xcode_link?(@project)
.project-action-button.project-xcode.inline<
diff --git a/changelogs/unreleased/35428-web-ide-button-missing-from-project-pages.yml b/changelogs/unreleased/35428-web-ide-button-missing-from-project-pages.yml
new file mode 100644
index 00000000000..c4b6f43cb48
--- /dev/null
+++ b/changelogs/unreleased/35428-web-ide-button-missing-from-project-pages.yml
@@ -0,0 +1,5 @@
+---
+title: Enable Web IDE on projects without Merge Requests
+merge_request: 24508
+author:
+type: fixed
diff --git a/changelogs/unreleased/dblessing_disable_enforced_sso_plan_expires.yml b/changelogs/unreleased/dblessing_disable_enforced_sso_plan_expires.yml
new file mode 100644
index 00000000000..5e202039513
--- /dev/null
+++ b/changelogs/unreleased/dblessing_disable_enforced_sso_plan_expires.yml
@@ -0,0 +1,5 @@
+---
+title: When a namespace GitLab Subscription expires, disable SSO enforcement
+merge_request: 21135
+author:
+type: fixed
diff --git a/changelogs/unreleased/georgekoltsov-fix-group-import-visibility-level.yml b/changelogs/unreleased/georgekoltsov-fix-group-import-visibility-level.yml
new file mode 100644
index 00000000000..61ec0dc6fc7
--- /dev/null
+++ b/changelogs/unreleased/georgekoltsov-fix-group-import-visibility-level.yml
@@ -0,0 +1,6 @@
+---
+title: Use closest allowed visibility level on group creation when importing groups
+ using Group Import/Export
+merge_request: 25026
+author:
+type: fixed
diff --git a/changelogs/unreleased/refactoring-entities-file-27.yml b/changelogs/unreleased/refactoring-entities-file-27.yml
new file mode 100644
index 00000000000..e4b76bfa8dc
--- /dev/null
+++ b/changelogs/unreleased/refactoring-entities-file-27.yml
@@ -0,0 +1,5 @@
+---
+title: Separate entities into own class files
+merge_request: 24985
+author: Rajendra Kadam
+type: added
diff --git a/changelogs/unreleased/refactoring-entities-file-29.yml b/changelogs/unreleased/refactoring-entities-file-29.yml
new file mode 100644
index 00000000000..633d7069406
--- /dev/null
+++ b/changelogs/unreleased/refactoring-entities-file-29.yml
@@ -0,0 +1,5 @@
+---
+title: Separate Application and Blob entities into own class files
+merge_request: 24997
+author: Rajendra Kadam
+type: added
diff --git a/changelogs/unreleased/remove-self-monitoring-feature-flag.yml b/changelogs/unreleased/remove-self-monitoring-feature-flag.yml
new file mode 100644
index 00000000000..2362bfaf5d1
--- /dev/null
+++ b/changelogs/unreleased/remove-self-monitoring-feature-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Remove self monitoring feature flag
+merge_request: 23631
+author:
+type: other
diff --git a/doc/development/go_guide/index.md b/doc/development/go_guide/index.md
index 27cd0370bba..73a1dd8ad8a 100644
--- a/doc/development/go_guide/index.md
+++ b/doc/development/go_guide/index.md
@@ -72,6 +72,7 @@ projects:
effects if the package is included multiple times.
- Use `go fmt` before committing ([Gofmt](https://golang.org/cmd/gofmt/) is a
tool that automatically formats Go source code).
+- Place private methods below the first caller method in the source file.
### Automatic linting
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index df7c7b15aeb..3435d1d6f11 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -199,50 +199,6 @@ module API
end.compact
end
end
-
- class UserAgentDetail < Grape::Entity
- expose :user_agent
- expose :ip_address
- expose :submitted, as: :akismet_submitted
- end
-
- class CustomAttribute < Grape::Entity
- expose :key
- expose :value
- end
-
- class PagesDomainCertificateExpiration < Grape::Entity
- expose :expired?, as: :expired
- expose :expiration
- end
-
- class Application < Grape::Entity
- expose :id
- expose :uid, as: :application_id
- expose :name, as: :application_name
- expose :redirect_uri, as: :callback_url
- expose :confidential
- end
-
- # Use with care, this exposes the secret
- class ApplicationWithSecret < Application
- expose :secret
- end
-
- class Blob < Grape::Entity
- expose :basename
- expose :data
- expose :path
- # TODO: :filename was renamed to :path but both still return the full path,
- # in the future we can only return the filename here without the leading
- # directory path.
- # https://gitlab.com/gitlab-org/gitlab/issues/34521
- expose :filename, &:path
- expose :id
- expose :ref
- expose :startline
- expose :project_id
- end
end
end
diff --git a/lib/api/entities/application.rb b/lib/api/entities/application.rb
new file mode 100644
index 00000000000..33514200424
--- /dev/null
+++ b/lib/api/entities/application.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Application < Grape::Entity
+ expose :id
+ expose :uid, as: :application_id
+ expose :name, as: :application_name
+ expose :redirect_uri, as: :callback_url
+ expose :confidential
+ end
+ end
+end
diff --git a/lib/api/entities/application_with_secret.rb b/lib/api/entities/application_with_secret.rb
new file mode 100644
index 00000000000..3e540381d89
--- /dev/null
+++ b/lib/api/entities/application_with_secret.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ # Use with care, this exposes the secret
+ class ApplicationWithSecret < Entities::Application
+ expose :secret
+ end
+ end
+end
diff --git a/lib/api/entities/blob.rb b/lib/api/entities/blob.rb
new file mode 100644
index 00000000000..b14ef127b68
--- /dev/null
+++ b/lib/api/entities/blob.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Blob < Grape::Entity
+ expose :basename
+ expose :data
+ expose :path
+ # TODO: :filename was renamed to :path but both still return the full path,
+ # in the future we can only return the filename here without the leading
+ # directory path.
+ # https://gitlab.com/gitlab-org/gitlab/issues/34521
+ expose :filename, &:path
+ expose :id
+ expose :ref
+ expose :startline
+ expose :project_id
+ end
+ end
+end
diff --git a/lib/api/entities/custom_attribute.rb b/lib/api/entities/custom_attribute.rb
new file mode 100644
index 00000000000..f949b709517
--- /dev/null
+++ b/lib/api/entities/custom_attribute.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class CustomAttribute < Grape::Entity
+ expose :key
+ expose :value
+ end
+ end
+end
diff --git a/lib/api/entities/pages_domain_certificate_expiration.rb b/lib/api/entities/pages_domain_certificate_expiration.rb
new file mode 100644
index 00000000000..bfc70f6657f
--- /dev/null
+++ b/lib/api/entities/pages_domain_certificate_expiration.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class PagesDomainCertificateExpiration < Grape::Entity
+ expose :expired?, as: :expired
+ expose :expiration
+ end
+ end
+end
diff --git a/lib/api/entities/user_agent_detail.rb b/lib/api/entities/user_agent_detail.rb
new file mode 100644
index 00000000000..a2d02c16589
--- /dev/null
+++ b/lib/api/entities/user_agent_detail.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class UserAgentDetail < Grape::Entity
+ expose :user_agent
+ expose :ip_address
+ expose :submitted, as: :akismet_submitted
+ end
+ end
+end
diff --git a/lib/api/group_import.rb b/lib/api/group_import.rb
index de7fdc27243..3531abb2604 100644
--- a/lib/api/group_import.rb
+++ b/lib/api/group_import.rb
@@ -5,15 +5,25 @@ module API
MAXIMUM_FILE_SIZE = 50.megabytes.freeze
helpers do
- def authorize_create_group!
- parent_group = find_group!(params[:parent_id]) if params[:parent_id].present?
+ def parent_group
+ find_group!(params[:parent_id]) if params[:parent_id].present?
+ end
+ def authorize_create_group!
if parent_group
authorize! :create_subgroup, parent_group
else
authorize! :create_group
end
end
+
+ def closest_allowed_visibility_level
+ if parent_group
+ Gitlab::VisibilityLevel.closest_allowed_level(parent_group.visibility_level)
+ else
+ Gitlab::VisibilityLevel::PRIVATE
+ end
+ end
end
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
@@ -59,6 +69,7 @@ module API
path: params[:path],
name: params[:name],
parent_id: params[:parent_id],
+ visibility_level: closest_allowed_visibility_level,
import_export_upload: ImportExportUpload.new(import_file: uploaded_file)
}
diff --git a/lib/gitlab/import_export/group_import_export.yml b/lib/gitlab/import_export/group_import_export.yml
index 7dcfbed8968..7203c1aa216 100644
--- a/lib/gitlab/import_export/group_import_export.yml
+++ b/lib/gitlab/import_export/group_import_export.yml
@@ -37,6 +37,7 @@ excluded_attributes:
- :runners_token
- :runners_token_encrypted
- :saml_discovery_token
+ - :visibility_level
methods:
labels:
diff --git a/lib/gitlab/import_export/group_tree_restorer.rb b/lib/gitlab/import_export/group_tree_restorer.rb
index 8230e4ff128..2f42843ed6c 100644
--- a/lib/gitlab/import_export/group_tree_restorer.rb
+++ b/lib/gitlab/import_export/group_tree_restorer.rb
@@ -74,12 +74,23 @@ module Gitlab
group_params = {
name: group_hash['name'],
path: group_hash['path'],
- parent_id: parent_group&.id
+ parent_id: parent_group&.id,
+ visibility_level: sub_group_visibility_level(group_hash, parent_group)
}
::Groups::CreateService.new(@user, group_params).execute
end
+ def sub_group_visibility_level(group_hash, parent_group)
+ original_visibility_level = group_hash['visibility_level'] || Gitlab::VisibilityLevel::PRIVATE
+
+ if parent_group && parent_group.visibility_level < original_visibility_level
+ Gitlab::VisibilityLevel.closest_allowed_level(parent_group.visibility_level)
+ else
+ original_visibility_level
+ end
+ end
+
def members_mapper
@members_mapper ||= Gitlab::ImportExport::MembersMapper.new(exported_members: @group_members, user: @user, importable: @group)
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f8d3c389bf7..b429e0d99d5 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -10130,6 +10130,9 @@ msgstr ""
msgid "IDE|Successful commit"
msgstr ""
+msgid "IDE|This option is disabled because you are not allowed to create merge requests in this project."
+msgstr ""
+
msgid "IDE|This option is disabled because you don't have write permissions for the current branch."
msgstr ""
@@ -17112,9 +17115,6 @@ msgstr ""
msgid "Self monitoring project does not exist"
msgstr ""
-msgid "Self-monitoring is not enabled on this GitLab server, contact your administrator."
-msgstr ""
-
msgid "Self-monitoring project does not exist. Please check logs for any error messages"
msgstr ""
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
index c619bd6d6a3..61047c42fcc 100644
--- a/qa/qa/page/project/show.rb
+++ b/qa/qa/page/project/show.rb
@@ -7,6 +7,14 @@ module QA
include Page::Component::ClonePanel
include Page::Project::SubMenus::Settings
+ view 'app/assets/javascripts/repository/components/table/row.vue' do
+ element :file_name_link
+ end
+
+ view 'app/assets/javascripts/repository/components/table/index.vue' do
+ element :file_tree_table
+ end
+
view 'app/views/layouts/header/_new_dropdown.haml' do
element :new_menu_toggle
element :new_issue_link, "link_to _('New issue'), new_project_issue_path(@project)" # rubocop:disable QA/ElementWithPattern
@@ -17,7 +25,8 @@ module QA
end
view 'app/views/projects/_home_panel.html.haml' do
- element :project_name
+ element :forked_from_link
+ element :project_name_content
end
view 'app/views/projects/_files.html.haml' do
@@ -37,10 +46,6 @@ module QA
element :quick_actions
end
- view 'app/views/projects/tree/_tree_content.html.haml' do
- element :file_tree
- end
-
view 'app/views/projects/tree/_tree_header.html.haml' do
element :add_to_tree
element :new_file_option
@@ -79,14 +84,18 @@ module QA
click_on 'Fork'
end
+ def forked_from?(parent_project_name)
+ has_element?(:forked_from_link, text: parent_project_name)
+ end
+
def click_file(filename)
- within_element(:file_tree) do
+ within_element(:file_tree_table) do
click_on filename
end
end
def click_commit(commit_msg)
- within_element(:file_tree) do
+ within_element(:file_tree_table) do
click_on commit_msg
end
end
@@ -96,6 +105,16 @@ module QA
click_link 'New issue'
end
+ def has_file?(name)
+ within_element(:file_tree_table) do
+ has_element?(:file_name_link, text: name)
+ end
+ end
+
+ def has_name?(name)
+ has_element?(:project_name_content, text: name)
+ end
+
def last_commit_content
find_element(:commit_content).text
end
@@ -113,7 +132,7 @@ module QA
end
def project_name
- find('.qa-project-name').text
+ find_element(:project_name_content).text
end
def switch_to_branch(branch_name)
diff --git a/qa/qa/resource/api_fabricator.rb b/qa/qa/resource/api_fabricator.rb
index 3c06f139738..cac58c599ea 100644
--- a/qa/qa/resource/api_fabricator.rb
+++ b/qa/qa/resource/api_fabricator.rb
@@ -8,10 +8,12 @@ module QA
module ApiFabricator
include Capybara::DSL
- ResourceNotFoundError = Class.new(RuntimeError)
ResourceFabricationFailedError = Class.new(RuntimeError)
- ResourceURLMissingError = Class.new(RuntimeError)
ResourceNotDeletedError = Class.new(RuntimeError)
+ ResourceNotFoundError = Class.new(RuntimeError)
+ ResourceQueryError = Class.new(RuntimeError)
+ ResourceUpdateFailedError = Class.new(RuntimeError)
+ ResourceURLMissingError = Class.new(RuntimeError)
attr_reader :api_resource, :api_response
attr_writer :api_client
diff --git a/qa/qa/resource/fork.rb b/qa/qa/resource/fork.rb
index 73f1b0b9695..854dd92e89c 100644
--- a/qa/qa/resource/fork.rb
+++ b/qa/qa/resource/fork.rb
@@ -3,19 +3,24 @@
module QA
module Resource
class Fork < Base
+ attribute :name do
+ upstream.name
+ end
+
attribute :project do
- Resource::Project.fabricate! do |resource|
- resource.name = upstream.project.name
- resource.path_with_namespace = "#{user.name}/#{upstream.project.name}"
+ Resource::Project.fabricate_via_api! do |resource|
+ resource.add_name_uuid = false
+ resource.name = name
+ resource.path_with_namespace = "#{user.username}/#{name}"
end
end
attribute :upstream do
- Repository::ProjectPush.fabricate!
+ Repository::ProjectPush.fabricate!.project
end
attribute :user do
- User.fabricate! do |resource|
+ User.fabricate_via_api! do |resource|
if Runtime::Env.forker?
resource.username = Runtime::Env.forker_username
resource.password = Runtime::Env.forker_password
@@ -33,7 +38,7 @@ module QA
login.sign_in_using_credentials(user: user)
end
- upstream.project.visit!
+ upstream.visit!
Page::Project::Show.perform(&:fork_project)
@@ -47,6 +52,41 @@ module QA
populate(:project)
end
+
+ def fabricate_via_api!
+ populate(:upstream, :user)
+
+ Runtime::Logger.debug("Forking project #{upstream.name} to namespace #{user.username}...")
+ super
+ wait_until_forked
+
+ populate(:project)
+ end
+
+ def api_get_path
+ "/projects/#{CGI.escape(path_with_namespace)}"
+ end
+
+ def api_post_path
+ "/projects/#{upstream.id}/fork"
+ end
+
+ def api_post_body
+ {
+ namespace: user.username,
+ name: name,
+ path: name
+ }
+ end
+
+ def wait_until_forked
+ Runtime::Logger.debug("Waiting for the fork process to complete...")
+ forked = wait_until do
+ project.import_status == "finished"
+ end
+
+ raise "Timed out while waiting for the fork process to complete." unless forked
+ end
end
end
end
diff --git a/qa/qa/resource/merge_request_from_fork.rb b/qa/qa/resource/merge_request_from_fork.rb
index 9cb4e6a49ca..d9c86b3b527 100644
--- a/qa/qa/resource/merge_request_from_fork.rb
+++ b/qa/qa/resource/merge_request_from_fork.rb
@@ -8,7 +8,7 @@ module QA
attr_accessor :fork_branch
attribute :fork do
- Fork.fabricate!
+ Fork.fabricate_via_browser_ui!
end
attribute :push do
diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb
index f2ca0e0b8fd..62e55e18e9b 100644
--- a/qa/qa/resource/project.rb
+++ b/qa/qa/resource/project.rb
@@ -94,6 +94,10 @@ module QA
"#{api_get_path}/runners"
end
+ def api_put_path
+ "/projects/#{id}"
+ end
+
def api_post_path
'/projects'
end
@@ -115,6 +119,35 @@ module QA
post_body
end
+ def change_repository_storage(new_storage)
+ put_body = { repository_storage: new_storage }
+ response = put Runtime::API::Request.new(api_client, api_put_path).url, put_body
+
+ unless response.code == HTTP_STATUS_OK
+ raise ResourceUpdateFailedError, "Could not change repository storage to #{new_storage}. Request returned (#{response.code}): `#{response}`."
+ end
+
+ wait_until do
+ reload!
+
+ api_response[:repository_storage] == new_storage
+ end
+ end
+
+ def import_status
+ response = get Runtime::API::Request.new(api_client, "/projects/#{id}/import").url
+
+ unless response.code == HTTP_STATUS_OK
+ raise ResourceQueryError, "Could not get import status. Request returned (#{response.code}): `#{response}`."
+ end
+
+ result = parse_body(response)
+
+ Runtime::Logger.error("Import failed: #{result[:import_error]}") if result[:import_status] == "failed"
+
+ result[:import_status]
+ end
+
def runners(tag_list: nil)
response = get Runtime::API::Request.new(api_client, "#{api_runners_path}?tag_list=#{tag_list.compact.join(',')}").url
parse_body(response)
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
index 6514e41e279..1c947b0329f 100644
--- a/qa/qa/runtime/env.rb
+++ b/qa/qa/runtime/env.rb
@@ -22,6 +22,10 @@ module QA
SUPPORTED_FEATURES
end
+ def additional_repository_storage
+ ENV['QA_ADDITIONAL_REPOSITORY_STORAGE']
+ end
+
def admin_password
ENV['GITLAB_ADMIN_PASSWORD']
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb
index 370bf30f3a4..7d4e6b7efbc 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb
@@ -6,7 +6,7 @@ module QA
it 'user forks a project, submits a merge request and maintainer merges it' do
Flow::Login.sign_in
- merge_request = Resource::MergeRequestFromFork.fabricate! do |merge_request|
+ merge_request = Resource::MergeRequestFromFork.fabricate_via_browser_ui! do |merge_request|
merge_request.fork_branch = 'feature-branch'
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/move_project_create_fork_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/move_project_create_fork_spec.rb
new file mode 100644
index 00000000000..13fe8918f97
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/move_project_create_fork_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Create' do
+ describe 'Gitaly repository storage', :orchestrated, :repository_storage, :requires_admin, quarantine: { type: :new } do
+ let(:user) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) }
+ let(:parent_project) do
+ Resource::Project.fabricate_via_api! do |project|
+ project.name = 'parent-project'
+ project.initialize_with_readme = true
+ end
+ end
+ let(:fork_project) do
+ Resource::Fork.fabricate_via_api! do |fork|
+ fork.user = user
+ fork.upstream = parent_project
+ end.project
+ end
+
+ before do
+ parent_project.add_member(user)
+ end
+
+ it 'creates a 2nd fork after moving the parent project' do
+ Flow::Login.sign_in(as: user)
+
+ fork_project.visit!
+
+ parent_project.change_repository_storage(QA::Runtime::Env.additional_repository_storage)
+
+ second_fork_project = Resource::Fork.fabricate_via_api! do |fork|
+ fork.name = "second-fork"
+ fork.user = user
+ fork.upstream = parent_project
+ end.project
+
+ Resource::Repository::ProjectPush.fabricate! do |push|
+ push.project = second_fork_project
+ push.file_name = 'new_file'
+ push.file_content = '# This is a new file'
+ push.commit_message = 'Add new file'
+ push.new_branch = false
+ end.project.visit!
+
+ Page::Project::Show.perform do |show|
+ expect(show).to have_file('new_file')
+ expect(show).to have_name(second_fork_project.name)
+ expect(show).to be_forked_from(parent_project.name)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/show/user_sees_collaboration_links_spec.rb b/spec/features/projects/show/user_sees_collaboration_links_spec.rb
index ff133b58f89..63fcec4f9b3 100644
--- a/spec/features/projects/show/user_sees_collaboration_links_spec.rb
+++ b/spec/features/projects/show/user_sees_collaboration_links_spec.rb
@@ -3,66 +3,96 @@
require 'spec_helper'
describe 'Projects > Show > Collaboration links', :js do
- let(:project) { create(:project, :repository) }
+ using RSpec::Parameterized::TableSyntax
+
+ let(:project) { create(:project, :repository, :public) }
let(:user) { create(:user) }
before do
- project.add_developer(user)
sign_in(user)
end
- it 'shows all the expected links' do
- visit project_path(project)
+ context 'with developer user' do
+ before do
+ project.add_developer(user)
+ end
- # The navigation bar
- page.within('.header-new') do
- find('.qa-new-menu-toggle').click
+ it 'shows all the expected links' do
+ visit project_path(project)
- aggregate_failures 'dropdown links in the navigation bar' do
- expect(page).to have_link('New issue')
- expect(page).to have_link('New merge request')
- expect(page).to have_link('New snippet', href: new_project_snippet_path(project))
- end
+ # The navigation bar
+ page.within('.header-new') do
+ find('.qa-new-menu-toggle').click
- find('.qa-new-menu-toggle').click
- end
+ aggregate_failures 'dropdown links in the navigation bar' do
+ expect(page).to have_link('New issue')
+ expect(page).to have_link('New merge request')
+ expect(page).to have_link('New snippet', href: new_project_snippet_path(project))
+ end
- # The dropdown above the tree
- page.within('.repo-breadcrumb') do
- find('.qa-add-to-tree').click
+ find('.qa-new-menu-toggle').click
+ end
- aggregate_failures 'dropdown links above the repo tree' do
- expect(page).to have_link('New file')
- expect(page).to have_link('Upload file')
- expect(page).to have_link('New directory')
- expect(page).to have_link('New branch')
- expect(page).to have_link('New tag')
+ # The dropdown above the tree
+ page.within('.repo-breadcrumb') do
+ find('.qa-add-to-tree').click
+
+ aggregate_failures 'dropdown links above the repo tree' do
+ expect(page).to have_link('New file')
+ expect(page).to have_link('Upload file')
+ expect(page).to have_link('New directory')
+ expect(page).to have_link('New branch')
+ expect(page).to have_link('New tag')
+ end
end
+
+ # The Web IDE
+ expect(page).to have_link('Web IDE')
end
- # The Web IDE
- expect(page).to have_link('Web IDE')
- end
+ it 'hides the links when the project is archived' do
+ project.update!(archived: true)
- it 'hides the links when the project is archived' do
- project.update!(archived: true)
+ visit project_path(project)
- visit project_path(project)
+ page.within('.header-new') do
+ find('.qa-new-menu-toggle').click
- page.within('.header-new') do
- find('.qa-new-menu-toggle').click
+ aggregate_failures 'dropdown links' do
+ expect(page).not_to have_link('New issue')
+ expect(page).not_to have_link('New merge request')
+ expect(page).not_to have_link('New snippet', href: new_project_snippet_path(project))
+ end
- aggregate_failures 'dropdown links' do
- expect(page).not_to have_link('New issue')
- expect(page).not_to have_link('New merge request')
- expect(page).not_to have_link('New snippet', href: new_project_snippet_path(project))
+ find('.qa-new-menu-toggle').click
end
- find('.qa-new-menu-toggle').click
+ expect(page).not_to have_selector('.qa-add-to-tree')
+
+ expect(page).not_to have_link('Web IDE')
end
+ end
- expect(page).not_to have_selector('.qa-add-to-tree')
+ context "Web IDE link" do
+ where(:merge_requests_access_level, :user_level, :expect_ide_link) do
+ ::ProjectFeature::DISABLED | :guest | false
+ ::ProjectFeature::DISABLED | :developer | true
+ ::ProjectFeature::PRIVATE | :guest | false
+ ::ProjectFeature::PRIVATE | :developer | true
+ ::ProjectFeature::ENABLED | :guest | true
+ ::ProjectFeature::ENABLED | :developer | true
+ end
- expect(page).not_to have_link('Web IDE')
+ with_them do
+ before do
+ project.project_feature.update!({ merge_requests_access_level: merge_requests_access_level })
+ project.add_user(user, user_level)
+ visit project_path(project)
+ end
+
+ it "updates Web IDE link" do
+ expect(page.has_link?('Web IDE')).to be(expect_ide_link)
+ end
+ end
end
end
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/internal/group.json b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/internal/group.json
new file mode 100644
index 00000000000..f747088f87e
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/internal/group.json
@@ -0,0 +1,166 @@
+{
+ "id": 283,
+ "name": "internal",
+ "path": "internal",
+ "owner_id": null,
+ "created_at": "2020-02-12T16:56:34.924Z",
+ "updated_at": "2020-02-12T16:56:38.710Z",
+ "description": "",
+ "avatar": {
+ "url": null
+ },
+ "membership_lock": false,
+ "share_with_group_lock": false,
+ "visibility_level": 10,
+ "request_access_enabled": true,
+ "ldap_sync_status": "ready",
+ "ldap_sync_error": null,
+ "ldap_sync_last_update_at": null,
+ "ldap_sync_last_successful_update_at": null,
+ "ldap_sync_last_sync_at": null,
+ "lfs_enabled": null,
+ "parent_id": null,
+ "shared_runners_minutes_limit": null,
+ "repository_size_limit": null,
+ "require_two_factor_authentication": false,
+ "two_factor_grace_period": 48,
+ "plan_id": null,
+ "project_creation_level": 2,
+ "trial_ends_on": null,
+ "file_template_project_id": null,
+ "custom_project_templates_group_id": null,
+ "auto_devops_enabled": null,
+ "extra_shared_runners_minutes_limit": null,
+ "last_ci_minutes_notification_at": null,
+ "last_ci_minutes_usage_notification_level": null,
+ "subgroup_creation_level": 1,
+ "emails_disabled": null,
+ "max_pages_size": null,
+ "max_artifacts_size": null,
+ "mentions_disabled": null,
+ "children": [
+ {
+ "id": 284,
+ "name": "public",
+ "path": "public",
+ "owner_id": null,
+ "created_at": "2020-02-12T17:33:00.575Z",
+ "updated_at": "2020-02-12T17:33:00.575Z",
+ "description": "",
+ "avatar": {
+ "url": null
+ },
+ "membership_lock": false,
+ "share_with_group_lock": false,
+ "visibility_level": 20,
+ "request_access_enabled": true,
+ "ldap_sync_status": "ready",
+ "ldap_sync_error": null,
+ "ldap_sync_last_update_at": null,
+ "ldap_sync_last_successful_update_at": null,
+ "ldap_sync_last_sync_at": null,
+ "lfs_enabled": null,
+ "parent_id": 283,
+ "shared_runners_minutes_limit": null,
+ "repository_size_limit": null,
+ "require_two_factor_authentication": false,
+ "two_factor_grace_period": 48,
+ "plan_id": null,
+ "project_creation_level": 2,
+ "trial_ends_on": null,
+ "file_template_project_id": null,
+ "custom_project_templates_group_id": null,
+ "auto_devops_enabled": null,
+ "extra_shared_runners_minutes_limit": null,
+ "last_ci_minutes_notification_at": null,
+ "last_ci_minutes_usage_notification_level": null,
+ "subgroup_creation_level": 1,
+ "emails_disabled": null,
+ "max_pages_size": null,
+ "max_artifacts_size": null,
+ "mentions_disabled": null
+ },
+ {
+ "id": 285,
+ "name": "internal",
+ "path": "internal",
+ "owner_id": null,
+ "created_at": "2020-02-12T17:33:00.575Z",
+ "updated_at": "2020-02-12T17:33:00.575Z",
+ "description": "",
+ "avatar": {
+ "url": null
+ },
+ "membership_lock": false,
+ "share_with_group_lock": false,
+ "visibility_level": 10,
+ "request_access_enabled": true,
+ "ldap_sync_status": "ready",
+ "ldap_sync_error": null,
+ "ldap_sync_last_update_at": null,
+ "ldap_sync_last_successful_update_at": null,
+ "ldap_sync_last_sync_at": null,
+ "lfs_enabled": null,
+ "parent_id": 283,
+ "shared_runners_minutes_limit": null,
+ "repository_size_limit": null,
+ "require_two_factor_authentication": false,
+ "two_factor_grace_period": 48,
+ "plan_id": null,
+ "project_creation_level": 2,
+ "trial_ends_on": null,
+ "file_template_project_id": null,
+ "custom_project_templates_group_id": null,
+ "auto_devops_enabled": null,
+ "extra_shared_runners_minutes_limit": null,
+ "last_ci_minutes_notification_at": null,
+ "last_ci_minutes_usage_notification_level": null,
+ "subgroup_creation_level": 1,
+ "emails_disabled": null,
+ "max_pages_size": null,
+ "max_artifacts_size": null,
+ "mentions_disabled": null
+ },
+ {
+ "id": 286,
+ "name": "private",
+ "path": "private",
+ "owner_id": null,
+ "created_at": "2020-02-12T17:33:00.575Z",
+ "updated_at": "2020-02-12T17:33:00.575Z",
+ "description": "",
+ "avatar": {
+ "url": null
+ },
+ "membership_lock": false,
+ "share_with_group_lock": false,
+ "visibility_level": 0,
+ "request_access_enabled": true,
+ "ldap_sync_status": "ready",
+ "ldap_sync_error": null,
+ "ldap_sync_last_update_at": null,
+ "ldap_sync_last_successful_update_at": null,
+ "ldap_sync_last_sync_at": null,
+ "lfs_enabled": null,
+ "parent_id": 283,
+ "shared_runners_minutes_limit": null,
+ "repository_size_limit": null,
+ "require_two_factor_authentication": false,
+ "two_factor_grace_period": 48,
+ "plan_id": null,
+ "project_creation_level": 2,
+ "trial_ends_on": null,
+ "file_template_project_id": null,
+ "custom_project_templates_group_id": null,
+ "auto_devops_enabled": null,
+ "extra_shared_runners_minutes_limit": null,
+ "last_ci_minutes_notification_at": null,
+ "last_ci_minutes_usage_notification_level": null,
+ "subgroup_creation_level": 1,
+ "emails_disabled": null,
+ "max_pages_size": null,
+ "max_artifacts_size": null,
+ "mentions_disabled": null
+ }
+ ]
+}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/private/group.json b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/private/group.json
new file mode 100644
index 00000000000..1328e596fa5
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/private/group.json
@@ -0,0 +1,166 @@
+{
+ "id": 283,
+ "name": "private",
+ "path": "private",
+ "owner_id": null,
+ "created_at": "2020-02-12T16:56:34.924Z",
+ "updated_at": "2020-02-12T16:56:38.710Z",
+ "description": "",
+ "avatar": {
+ "url": null
+ },
+ "membership_lock": false,
+ "share_with_group_lock": false,
+ "visibility_level": 0,
+ "request_access_enabled": true,
+ "ldap_sync_status": "ready",
+ "ldap_sync_error": null,
+ "ldap_sync_last_update_at": null,
+ "ldap_sync_last_successful_update_at": null,
+ "ldap_sync_last_sync_at": null,
+ "lfs_enabled": null,
+ "parent_id": null,
+ "shared_runners_minutes_limit": null,
+ "repository_size_limit": null,
+ "require_two_factor_authentication": false,
+ "two_factor_grace_period": 48,
+ "plan_id": null,
+ "project_creation_level": 2,
+ "trial_ends_on": null,
+ "file_template_project_id": null,
+ "custom_project_templates_group_id": null,
+ "auto_devops_enabled": null,
+ "extra_shared_runners_minutes_limit": null,
+ "last_ci_minutes_notification_at": null,
+ "last_ci_minutes_usage_notification_level": null,
+ "subgroup_creation_level": 1,
+ "emails_disabled": null,
+ "max_pages_size": null,
+ "max_artifacts_size": null,
+ "mentions_disabled": null,
+ "children": [
+ {
+ "id": 284,
+ "name": "public",
+ "path": "public",
+ "owner_id": null,
+ "created_at": "2020-02-12T17:33:00.575Z",
+ "updated_at": "2020-02-12T17:33:00.575Z",
+ "description": "",
+ "avatar": {
+ "url": null
+ },
+ "membership_lock": false,
+ "share_with_group_lock": false,
+ "visibility_level": 20,
+ "request_access_enabled": true,
+ "ldap_sync_status": "ready",
+ "ldap_sync_error": null,
+ "ldap_sync_last_update_at": null,
+ "ldap_sync_last_successful_update_at": null,
+ "ldap_sync_last_sync_at": null,
+ "lfs_enabled": null,
+ "parent_id": 283,
+ "shared_runners_minutes_limit": null,
+ "repository_size_limit": null,
+ "require_two_factor_authentication": false,
+ "two_factor_grace_period": 48,
+ "plan_id": null,
+ "project_creation_level": 2,
+ "trial_ends_on": null,
+ "file_template_project_id": null,
+ "custom_project_templates_group_id": null,
+ "auto_devops_enabled": null,
+ "extra_shared_runners_minutes_limit": null,
+ "last_ci_minutes_notification_at": null,
+ "last_ci_minutes_usage_notification_level": null,
+ "subgroup_creation_level": 1,
+ "emails_disabled": null,
+ "max_pages_size": null,
+ "max_artifacts_size": null,
+ "mentions_disabled": null
+ },
+ {
+ "id": 285,
+ "name": "internal",
+ "path": "internal",
+ "owner_id": null,
+ "created_at": "2020-02-12T17:33:00.575Z",
+ "updated_at": "2020-02-12T17:33:00.575Z",
+ "description": "",
+ "avatar": {
+ "url": null
+ },
+ "membership_lock": false,
+ "share_with_group_lock": false,
+ "visibility_level": 10,
+ "request_access_enabled": true,
+ "ldap_sync_status": "ready",
+ "ldap_sync_error": null,
+ "ldap_sync_last_update_at": null,
+ "ldap_sync_last_successful_update_at": null,
+ "ldap_sync_last_sync_at": null,
+ "lfs_enabled": null,
+ "parent_id": 283,
+ "shared_runners_minutes_limit": null,
+ "repository_size_limit": null,
+ "require_two_factor_authentication": false,
+ "two_factor_grace_period": 48,
+ "plan_id": null,
+ "project_creation_level": 2,
+ "trial_ends_on": null,
+ "file_template_project_id": null,
+ "custom_project_templates_group_id": null,
+ "auto_devops_enabled": null,
+ "extra_shared_runners_minutes_limit": null,
+ "last_ci_minutes_notification_at": null,
+ "last_ci_minutes_usage_notification_level": null,
+ "subgroup_creation_level": 1,
+ "emails_disabled": null,
+ "max_pages_size": null,
+ "max_artifacts_size": null,
+ "mentions_disabled": null
+ },
+ {
+ "id": 286,
+ "name": "private",
+ "path": "private",
+ "owner_id": null,
+ "created_at": "2020-02-12T17:33:00.575Z",
+ "updated_at": "2020-02-12T17:33:00.575Z",
+ "description": "",
+ "avatar": {
+ "url": null
+ },
+ "membership_lock": false,
+ "share_with_group_lock": false,
+ "visibility_level": 0,
+ "request_access_enabled": true,
+ "ldap_sync_status": "ready",
+ "ldap_sync_error": null,
+ "ldap_sync_last_update_at": null,
+ "ldap_sync_last_successful_update_at": null,
+ "ldap_sync_last_sync_at": null,
+ "lfs_enabled": null,
+ "parent_id": 283,
+ "shared_runners_minutes_limit": null,
+ "repository_size_limit": null,
+ "require_two_factor_authentication": false,
+ "two_factor_grace_period": 48,
+ "plan_id": null,
+ "project_creation_level": 2,
+ "trial_ends_on": null,
+ "file_template_project_id": null,
+ "custom_project_templates_group_id": null,
+ "auto_devops_enabled": null,
+ "extra_shared_runners_minutes_limit": null,
+ "last_ci_minutes_notification_at": null,
+ "last_ci_minutes_usage_notification_level": null,
+ "subgroup_creation_level": 1,
+ "emails_disabled": null,
+ "max_pages_size": null,
+ "max_artifacts_size": null,
+ "mentions_disabled": null
+ }
+ ]
+}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/public/group.json b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/public/group.json
new file mode 100644
index 00000000000..29020e92004
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/public/group.json
@@ -0,0 +1,166 @@
+{
+ "id": 283,
+ "name": "public",
+ "path": "public",
+ "owner_id": null,
+ "created_at": "2020-02-12T16:56:34.924Z",
+ "updated_at": "2020-02-12T16:56:38.710Z",
+ "description": "",
+ "avatar": {
+ "url": null
+ },
+ "membership_lock": false,
+ "share_with_group_lock": false,
+ "visibility_level": 20,
+ "request_access_enabled": true,
+ "ldap_sync_status": "ready",
+ "ldap_sync_error": null,
+ "ldap_sync_last_update_at": null,
+ "ldap_sync_last_successful_update_at": null,
+ "ldap_sync_last_sync_at": null,
+ "lfs_enabled": null,
+ "parent_id": null,
+ "shared_runners_minutes_limit": null,
+ "repository_size_limit": null,
+ "require_two_factor_authentication": false,
+ "two_factor_grace_period": 48,
+ "plan_id": null,
+ "project_creation_level": 2,
+ "trial_ends_on": null,
+ "file_template_project_id": null,
+ "custom_project_templates_group_id": null,
+ "auto_devops_enabled": null,
+ "extra_shared_runners_minutes_limit": null,
+ "last_ci_minutes_notification_at": null,
+ "last_ci_minutes_usage_notification_level": null,
+ "subgroup_creation_level": 1,
+ "emails_disabled": null,
+ "max_pages_size": null,
+ "max_artifacts_size": null,
+ "mentions_disabled": null,
+ "children": [
+ {
+ "id": 284,
+ "name": "public",
+ "path": "public",
+ "owner_id": null,
+ "created_at": "2020-02-12T17:33:00.575Z",
+ "updated_at": "2020-02-12T17:33:00.575Z",
+ "description": "",
+ "avatar": {
+ "url": null
+ },
+ "membership_lock": false,
+ "share_with_group_lock": false,
+ "visibility_level": 20,
+ "request_access_enabled": true,
+ "ldap_sync_status": "ready",
+ "ldap_sync_error": null,
+ "ldap_sync_last_update_at": null,
+ "ldap_sync_last_successful_update_at": null,
+ "ldap_sync_last_sync_at": null,
+ "lfs_enabled": null,
+ "parent_id": 283,
+ "shared_runners_minutes_limit": null,
+ "repository_size_limit": null,
+ "require_two_factor_authentication": false,
+ "two_factor_grace_period": 48,
+ "plan_id": null,
+ "project_creation_level": 2,
+ "trial_ends_on": null,
+ "file_template_project_id": null,
+ "custom_project_templates_group_id": null,
+ "auto_devops_enabled": null,
+ "extra_shared_runners_minutes_limit": null,
+ "last_ci_minutes_notification_at": null,
+ "last_ci_minutes_usage_notification_level": null,
+ "subgroup_creation_level": 1,
+ "emails_disabled": null,
+ "max_pages_size": null,
+ "max_artifacts_size": null,
+ "mentions_disabled": null
+ },
+ {
+ "id": 285,
+ "name": "internal",
+ "path": "internal",
+ "owner_id": null,
+ "created_at": "2020-02-12T17:33:00.575Z",
+ "updated_at": "2020-02-12T17:33:00.575Z",
+ "description": "",
+ "avatar": {
+ "url": null
+ },
+ "membership_lock": false,
+ "share_with_group_lock": false,
+ "visibility_level": 10,
+ "request_access_enabled": true,
+ "ldap_sync_status": "ready",
+ "ldap_sync_error": null,
+ "ldap_sync_last_update_at": null,
+ "ldap_sync_last_successful_update_at": null,
+ "ldap_sync_last_sync_at": null,
+ "lfs_enabled": null,
+ "parent_id": 283,
+ "shared_runners_minutes_limit": null,
+ "repository_size_limit": null,
+ "require_two_factor_authentication": false,
+ "two_factor_grace_period": 48,
+ "plan_id": null,
+ "project_creation_level": 2,
+ "trial_ends_on": null,
+ "file_template_project_id": null,
+ "custom_project_templates_group_id": null,
+ "auto_devops_enabled": null,
+ "extra_shared_runners_minutes_limit": null,
+ "last_ci_minutes_notification_at": null,
+ "last_ci_minutes_usage_notification_level": null,
+ "subgroup_creation_level": 1,
+ "emails_disabled": null,
+ "max_pages_size": null,
+ "max_artifacts_size": null,
+ "mentions_disabled": null
+ },
+ {
+ "id": 286,
+ "name": "private",
+ "path": "private",
+ "owner_id": null,
+ "created_at": "2020-02-12T17:33:00.575Z",
+ "updated_at": "2020-02-12T17:33:00.575Z",
+ "description": "",
+ "avatar": {
+ "url": null
+ },
+ "membership_lock": false,
+ "share_with_group_lock": false,
+ "visibility_level": 0,
+ "request_access_enabled": true,
+ "ldap_sync_status": "ready",
+ "ldap_sync_error": null,
+ "ldap_sync_last_update_at": null,
+ "ldap_sync_last_successful_update_at": null,
+ "ldap_sync_last_sync_at": null,
+ "lfs_enabled": null,
+ "parent_id": 283,
+ "shared_runners_minutes_limit": null,
+ "repository_size_limit": null,
+ "require_two_factor_authentication": false,
+ "two_factor_grace_period": 48,
+ "plan_id": null,
+ "project_creation_level": 2,
+ "trial_ends_on": null,
+ "file_template_project_id": null,
+ "custom_project_templates_group_id": null,
+ "auto_devops_enabled": null,
+ "extra_shared_runners_minutes_limit": null,
+ "last_ci_minutes_notification_at": null,
+ "last_ci_minutes_usage_notification_level": null,
+ "subgroup_creation_level": 1,
+ "emails_disabled": null,
+ "max_pages_size": null,
+ "max_artifacts_size": null,
+ "mentions_disabled": null
+ }
+ ]
+}
diff --git a/spec/fixtures/lsif.json.gz b/spec/fixtures/lsif.json.gz
index 275a87e738b..3f74588cd7e 100644
--- a/spec/fixtures/lsif.json.gz
+++ b/spec/fixtures/lsif.json.gz
Binary files differ
diff --git a/spec/frontend/ide/mock_data.js b/spec/frontend/ide/mock_data.js
index a1b57dca6bc..472516b6a2c 100644
--- a/spec/frontend/ide/mock_data.js
+++ b/spec/frontend/ide/mock_data.js
@@ -18,6 +18,7 @@ export const projectData = {
},
mergeRequests: {},
merge_requests_enabled: true,
+ userPermissions: {},
default_branch: 'master',
};
diff --git a/spec/frontend/ide/services/index_spec.js b/spec/frontend/ide/services/index_spec.js
index 83a3cfe618b..55f174f4663 100644
--- a/spec/frontend/ide/services/index_spec.js
+++ b/spec/frontend/ide/services/index_spec.js
@@ -2,11 +2,17 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import services from '~/ide/services';
import Api from '~/api';
+import gqClient from '~/ide/services/gql';
import { escapeFileUrl } from '~/lib/utils/url_utility';
+import getUserPermissions from '~/ide/queries/getUserPermissions.query.graphql';
+import { projectData } from '../mock_data';
jest.mock('~/api');
+jest.mock('~/ide/services/gql');
-const TEST_PROJECT_ID = 'alice/wonderland';
+const TEST_NAMESPACE = 'alice';
+const TEST_PROJECT = 'wonderland';
+const TEST_PROJECT_ID = `${TEST_NAMESPACE}/${TEST_PROJECT}`;
const TEST_BRANCH = 'master-patch-123';
const TEST_COMMIT_SHA = '123456789';
const TEST_FILE_PATH = 'README2.md';
@@ -111,4 +117,27 @@ describe('IDE services', () => {
},
);
});
+
+ describe('getProjectData', () => {
+ it('combines gql and API requests', () => {
+ const gqlProjectData = {
+ userPermissions: {
+ bogus: true,
+ },
+ };
+ Api.project.mockReturnValue(Promise.resolve({ data: { ...projectData } }));
+ gqClient.query.mockReturnValue(Promise.resolve({ data: { project: gqlProjectData } }));
+
+ return services.getProjectData(TEST_NAMESPACE, TEST_PROJECT).then(response => {
+ expect(response).toEqual({ data: { ...projectData, ...gqlProjectData } });
+ expect(Api.project).toHaveBeenCalledWith(TEST_PROJECT_ID);
+ expect(gqClient.query).toHaveBeenCalledWith({
+ query: getUserPermissions,
+ variables: {
+ projectPath: TEST_PROJECT_ID,
+ },
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/ide/stores/getters_spec.js b/spec/frontend/ide/stores/getters_spec.js
index 21c5e886738..011be95c1d2 100644
--- a/spec/frontend/ide/stores/getters_spec.js
+++ b/spec/frontend/ide/stores/getters_spec.js
@@ -2,6 +2,8 @@ import * as getters from '~/ide/stores/getters';
import { createStore } from '~/ide/stores';
import { file } from '../helpers';
+const TEST_PROJECT_ID = 'test_project';
+
describe('IDE store getters', () => {
let localState;
let localStore;
@@ -398,4 +400,38 @@ describe('IDE store getters', () => {
},
);
});
+
+ describe('findProjectPermissions', () => {
+ it('returns false if project not found', () => {
+ expect(localStore.getters.findProjectPermissions(TEST_PROJECT_ID)).toEqual({});
+ });
+
+ it('finds permission in given project', () => {
+ const userPermissions = {
+ readMergeRequest: true,
+ createMergeRequestsIn: false,
+ };
+
+ localState.projects[TEST_PROJECT_ID] = { userPermissions };
+
+ expect(localStore.getters.findProjectPermissions(TEST_PROJECT_ID)).toBe(userPermissions);
+ });
+ });
+
+ describe.each`
+ getterName | permissionKey
+ ${'canReadMergeRequests'} | ${'readMergeRequest'}
+ ${'canCreateMergeRequests'} | ${'createMergeRequestIn'}
+ `('$getterName', ({ getterName, permissionKey }) => {
+ it.each([true, false])('finds permission for current project (%s)', val => {
+ localState.projects[TEST_PROJECT_ID] = {
+ userPermissions: {
+ [permissionKey]: val,
+ },
+ };
+ localState.currentProjectId = TEST_PROJECT_ID;
+
+ expect(localStore.getters[getterName]).toBe(val);
+ });
+ });
});
diff --git a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
index 49feae0884e..2d411fcce79 100644
--- a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
+++ b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
@@ -15,6 +15,7 @@ exports[`Repository table row component renders table row 1`] = `
<a
class="str-truncated"
+ data-qa-selector="file_name_link"
href="https://test.com"
>
@@ -64,6 +65,7 @@ exports[`Repository table row component renders table row for path with special
<a
class="str-truncated"
+ data-qa-selector="file_name_link"
href="https://test.com"
>
diff --git a/spec/javascripts/ide/components/commit_sidebar/new_merge_request_option_spec.js b/spec/javascripts/ide/components/commit_sidebar/new_merge_request_option_spec.js
index 02caf689c50..7c0b4000229 100644
--- a/spec/javascripts/ide/components/commit_sidebar/new_merge_request_option_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/new_merge_request_option_spec.js
@@ -1,13 +1,15 @@
import Vue from 'vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { projectData, branches } from 'spec/ide/mock_data';
-import { resetStore } from 'spec/ide/helpers';
import NewMergeRequestOption from '~/ide/components/commit_sidebar/new_merge_request_option.vue';
-import store from '~/ide/stores';
-import consts from '../../../../../app/assets/javascripts/ide/stores/modules/commit/constants';
+import { createStore } from '~/ide/stores';
+import { PERMISSION_CREATE_MR } from '~/ide/constants';
+import consts from '~/ide/stores/modules/commit/constants';
describe('create new MR checkbox', () => {
+ let store;
let vm;
+
const setMR = () => {
vm.$store.state.currentMergeRequestId = '1';
vm.$store.state.projects[store.state.currentProjectId].mergeRequests[
@@ -15,6 +17,10 @@ describe('create new MR checkbox', () => {
] = { foo: 'bar' };
};
+ const setPermissions = permissions => {
+ store.state.projects[store.state.currentProjectId].userPermissions = permissions;
+ };
+
const createComponent = ({ currentBranchId = 'master', createNewBranch = false } = {}) => {
const Component = Vue.extend(NewMergeRequestOption);
@@ -25,20 +31,29 @@ describe('create new MR checkbox', () => {
: consts.COMMIT_TO_CURRENT_BRANCH;
vm.$store.state.currentBranchId = currentBranchId;
- vm.$store.state.currentProjectId = 'abcproject';
- const proj = JSON.parse(JSON.stringify(projectData));
- proj.branches[currentBranchId] = branches.find(branch => branch.name === currentBranchId);
-
- Vue.set(vm.$store.state.projects, 'abcproject', proj);
+ store.state.projects.abcproject.branches[currentBranchId] = branches.find(
+ branch => branch.name === currentBranchId,
+ );
return vm.$mount();
};
+ const findInput = () => vm.$el.querySelector('input[type="checkbox"]');
+ const findLabel = () => vm.$el.querySelector('.js-ide-commit-new-mr');
+
+ beforeEach(() => {
+ store = createStore();
+
+ store.state.currentProjectId = 'abcproject';
+
+ const proj = JSON.parse(JSON.stringify(projectData));
+ proj.userPermissions[PERMISSION_CREATE_MR] = true;
+ Vue.set(store.state.projects, 'abcproject', proj);
+ });
+
afterEach(() => {
vm.$destroy();
-
- resetStore(vm.$store);
});
describe('for default branch', () => {
@@ -160,6 +175,24 @@ describe('create new MR checkbox', () => {
.then(done)
.catch(done.fail);
});
+
+ it('shows enablded checkbox', () => {
+ expect(findLabel().classList.contains('is-disabled')).toBe(false);
+ expect(findInput().disabled).toBe(false);
+ });
+ });
+
+ describe('when user cannot create MR', () => {
+ beforeEach(() => {
+ setPermissions({ [PERMISSION_CREATE_MR]: false });
+
+ createComponent({ currentBranchId: 'regular' });
+ });
+
+ it('disabled checkbox', () => {
+ expect(findLabel().classList.contains('is-disabled')).toBe(true);
+ expect(findInput().disabled).toBe(true);
+ });
});
it('dispatches toggleShouldCreateMR when clicking checkbox', () => {
diff --git a/spec/javascripts/ide/components/nav_dropdown_button_spec.js b/spec/javascripts/ide/components/nav_dropdown_button_spec.js
index 0d63869fba2..bbaf97164ea 100644
--- a/spec/javascripts/ide/components/nav_dropdown_button_spec.js
+++ b/spec/javascripts/ide/components/nav_dropdown_button_spec.js
@@ -2,62 +2,92 @@ import Vue from 'vue';
import { trimText } from 'spec/helpers/text_helper';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import NavDropdownButton from '~/ide/components/nav_dropdown_button.vue';
-import store from '~/ide/stores';
-import { resetStore } from '../helpers';
+import { createStore } from '~/ide/stores';
describe('NavDropdown', () => {
const TEST_BRANCH_ID = 'lorem-ipsum-dolar';
const TEST_MR_ID = '12345';
- const Component = Vue.extend(NavDropdownButton);
+ let store;
let vm;
beforeEach(() => {
- vm = mountComponentWithStore(Component, { store });
-
- vm.$mount();
+ store = createStore();
});
afterEach(() => {
vm.$destroy();
-
- resetStore(store);
});
- it('renders empty placeholders, if state is falsey', () => {
- expect(trimText(vm.$el.textContent)).toEqual('- -');
- });
+ const createComponent = (props = {}) => {
+ vm = mountComponentWithStore(Vue.extend(NavDropdownButton), { props, store });
+ vm.$mount();
+ };
- it('renders branch name, if state has currentBranchId', done => {
- vm.$store.state.currentBranchId = TEST_BRANCH_ID;
+ const findIcon = name => vm.$el.querySelector(`.ic-${name}`);
+ const findMRIcon = () => findIcon('merge-request');
+ const findBranchIcon = () => findIcon('branch');
- vm.$nextTick()
- .then(() => {
- expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} -`);
- })
- .then(done)
- .catch(done.fail);
- });
+ describe('normal', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders empty placeholders, if state is falsey', () => {
+ expect(trimText(vm.$el.textContent)).toEqual('- -');
+ });
- it('renders mr id, if state has currentMergeRequestId', done => {
- vm.$store.state.currentMergeRequestId = TEST_MR_ID;
+ it('renders branch name, if state has currentBranchId', done => {
+ vm.$store.state.currentBranchId = TEST_BRANCH_ID;
- vm.$nextTick()
- .then(() => {
- expect(trimText(vm.$el.textContent)).toEqual(`- !${TEST_MR_ID}`);
- })
- .then(done)
- .catch(done.fail);
+ vm.$nextTick()
+ .then(() => {
+ expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} -`);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('renders mr id, if state has currentMergeRequestId', done => {
+ vm.$store.state.currentMergeRequestId = TEST_MR_ID;
+
+ vm.$nextTick()
+ .then(() => {
+ expect(trimText(vm.$el.textContent)).toEqual(`- !${TEST_MR_ID}`);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('renders branch and mr, if state has both', done => {
+ vm.$store.state.currentBranchId = TEST_BRANCH_ID;
+ vm.$store.state.currentMergeRequestId = TEST_MR_ID;
+
+ vm.$nextTick()
+ .then(() => {
+ expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} !${TEST_MR_ID}`);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('shows icons', () => {
+ expect(findBranchIcon()).toBeTruthy();
+ expect(findMRIcon()).toBeTruthy();
+ });
});
- it('renders branch and mr, if state has both', done => {
- vm.$store.state.currentBranchId = TEST_BRANCH_ID;
- vm.$store.state.currentMergeRequestId = TEST_MR_ID;
+ describe('with showMergeRequests false', () => {
+ beforeEach(() => {
+ createComponent({ showMergeRequests: false });
+ });
+
+ it('shows single empty placeholder, if state is falsey', () => {
+ expect(trimText(vm.$el.textContent)).toEqual('-');
+ });
- vm.$nextTick()
- .then(() => {
- expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} !${TEST_MR_ID}`);
- })
- .then(done)
- .catch(done.fail);
+ it('shows only branch icon', () => {
+ expect(findBranchIcon()).toBeTruthy();
+ expect(findMRIcon()).toBe(null);
+ });
});
});
diff --git a/spec/javascripts/ide/components/nav_dropdown_spec.js b/spec/javascripts/ide/components/nav_dropdown_spec.js
index fe1d0ca371d..dfb4d03540f 100644
--- a/spec/javascripts/ide/components/nav_dropdown_spec.js
+++ b/spec/javascripts/ide/components/nav_dropdown_spec.js
@@ -3,6 +3,9 @@ import Vue from 'vue';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import store from '~/ide/stores';
import NavDropdown from '~/ide/components/nav_dropdown.vue';
+import { PERMISSION_READ_MR } from '~/ide/constants';
+
+const TEST_PROJECT_ID = 'lorem-ipsum';
describe('IDE NavDropdown', () => {
const Component = Vue.extend(NavDropdown);
@@ -10,6 +13,12 @@ describe('IDE NavDropdown', () => {
let $dropdown;
beforeEach(() => {
+ store.state.currentProjectId = TEST_PROJECT_ID;
+ Vue.set(store.state.projects, TEST_PROJECT_ID, {
+ userPermissions: {
+ [PERMISSION_READ_MR]: true,
+ },
+ });
vm = mountComponentWithStore(Component, { store });
$dropdown = $(vm.$el);
@@ -21,6 +30,9 @@ describe('IDE NavDropdown', () => {
vm.$destroy();
});
+ const findIcon = name => vm.$el.querySelector(`.ic-${name}`);
+ const findMRIcon = () => findIcon('merge-request');
+
it('renders nothing initially', () => {
expect(vm.$el).not.toContainElement('.ide-nav-form');
});
@@ -47,4 +59,22 @@ describe('IDE NavDropdown', () => {
.then(done)
.catch(done.fail);
});
+
+ it('renders merge request icon', () => {
+ expect(findMRIcon()).not.toBeNull();
+ });
+
+ describe('when user cannot read merge requests', () => {
+ beforeEach(done => {
+ store.state.projects[TEST_PROJECT_ID].userPermissions = {};
+
+ vm.$nextTick()
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does not render merge requests', () => {
+ expect(findMRIcon()).toBeNull();
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/actions/merge_request_spec.js b/spec/javascripts/ide/stores/actions/merge_request_spec.js
index 498e5142f0c..4917984cd42 100644
--- a/spec/javascripts/ide/stores/actions/merge_request_spec.js
+++ b/spec/javascripts/ide/stores/actions/merge_request_spec.js
@@ -8,7 +8,7 @@ import actions, {
openMergeRequest,
} from '~/ide/stores/actions/merge_request';
import service from '~/ide/services';
-import { activityBarViews } from '~/ide/constants';
+import { activityBarViews, PERMISSION_READ_MR } from '~/ide/constants';
import { resetStore } from '../../helpers';
const TEST_PROJECT = 'abcproject';
@@ -23,6 +23,9 @@ describe('IDE store merge request actions', () => {
store.state.projects[TEST_PROJECT] = {
id: TEST_PROJECT_ID,
mergeRequests: {},
+ userPermissions: {
+ [PERMISSION_READ_MR]: true,
+ },
};
});
@@ -79,6 +82,19 @@ describe('IDE store merge request actions', () => {
})
.catch(done.fail);
});
+
+ it('does nothing if user cannot read MRs', done => {
+ store.state.projects[TEST_PROJECT].userPermissions[PERMISSION_READ_MR] = false;
+
+ store
+ .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' })
+ .then(() => {
+ expect(service.getProjectMergeRequests).not.toHaveBeenCalled();
+ expect(store.state.currentMergeRequestId).toBe('');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
});
describe('no merge requests for branch available case', () => {
diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
index 056822bcfe5..fb8cb300209 100644
--- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js
+++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
@@ -7,7 +7,7 @@ import eventHub from '~/ide/eventhub';
import consts from '~/ide/stores/modules/commit/constants';
import * as mutationTypes from '~/ide/stores/modules/commit/mutation_types';
import * as actions from '~/ide/stores/modules/commit/actions';
-import { commitActionTypes } from '~/ide/constants';
+import { commitActionTypes, PERMISSION_CREATE_MR } from '~/ide/constants';
import testAction from '../../../../helpers/vuex_action_helper';
const TEST_COMMIT_SHA = '123456789';
@@ -313,6 +313,9 @@ describe('IDE commit module actions', () => {
},
},
},
+ userPermissions: {
+ [PERMISSION_CREATE_MR]: true,
+ },
},
},
});
diff --git a/spec/lib/gitlab/import_export/group_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/group_tree_restorer_spec.rb
index 9aa9471d155..d6bda7507c6 100644
--- a/spec/lib/gitlab/import_export/group_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/group_tree_restorer_spec.rb
@@ -125,4 +125,31 @@ describe Gitlab::ImportExport::GroupTreeRestorer do
end
end
end
+
+ context 'group visibility levels' do
+ let(:user) { create(:user) }
+ let(:shared) { Gitlab::ImportExport::Shared.new(group) }
+ let(:group_tree_restorer) { described_class.new(user: user, shared: shared, group: group, group_hash: nil) }
+
+ before do
+ setup_import_export_config(filepath)
+
+ group_tree_restorer.restore
+ end
+
+ shared_examples 'with visibility level' do |visibility_level, expected_visibilities|
+ context "when visibility level is #{visibility_level}" do
+ let(:group) { create(:group, visibility_level) }
+ let(:filepath) { "group_exports/visibility_levels/#{visibility_level}" }
+
+ it "imports all subgroups as #{visibility_level}" do
+ expect(group.children.map(&:visibility_level)).to eq(expected_visibilities)
+ end
+ end
+ end
+
+ include_examples 'with visibility level', :public, [20, 10, 0]
+ include_examples 'with visibility level', :private, [0, 0, 0]
+ include_examples 'with visibility level', :internal, [10, 10, 0]
+ end
end
diff --git a/spec/lib/gitlab/import_export/group_tree_saver_spec.rb b/spec/lib/gitlab/import_export/group_tree_saver_spec.rb
index d6aea8d1e69..d5ab8ca1e8d 100644
--- a/spec/lib/gitlab/import_export/group_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/group_tree_saver_spec.rb
@@ -80,7 +80,7 @@ describe Gitlab::ImportExport::GroupTreeSaver do
end
it 'saves the correct json' do
- expect(saved_group_json).to include({ 'description' => 'description', 'visibility_level' => 20 })
+ expect(saved_group_json).to include({ 'description' => 'description' })
end
it 'has milestones' do
diff --git a/spec/requests/api/group_import_spec.rb b/spec/requests/api/group_import_spec.rb
index 016ed6ff491..1594881677f 100644
--- a/spec/requests/api/group_import_spec.rb
+++ b/spec/requests/api/group_import_spec.rb
@@ -45,6 +45,14 @@ describe API::GroupImport do
expect(response).to have_gitlab_http_status(202)
end
+ it 'creates private group' do
+ expect { subject }.to change { Group.count }.by(1)
+
+ group = Group.find_by(name: 'test-import-group')
+
+ expect(group.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+
context 'when importing to a parent group' do
before do
group.add_owner(user)
@@ -59,6 +67,34 @@ describe API::GroupImport do
expect(group.children.count).to eq(1)
end
+ context 'when parent group is private or internal' do
+ let(:public_parent_group) { create(:group, :public) }
+ let(:internal_parent_group) { create(:group, :internal) }
+
+ before do
+ public_parent_group.add_owner(user)
+ internal_parent_group.add_owner(user)
+ end
+
+ it 'imports public group' do
+ params[:parent_id] = public_parent_group.id
+
+ subject
+
+ expect(response).to have_gitlab_http_status(202)
+ expect(public_parent_group.children.first.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it 'imports internal group' do
+ params[:parent_id] = internal_parent_group.id
+
+ subject
+
+ expect(response).to have_gitlab_http_status(202)
+ expect(internal_parent_group.children.first.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
+ end
+ end
+
context 'when parent group is invalid' do
it 'returns 404 and does not create new group' do
params[:parent_id] = 99999
diff --git a/spec/requests/api/lsif_data_spec.rb b/spec/requests/api/lsif_data_spec.rb
index 2e0670ded95..214bc832cda 100644
--- a/spec/requests/api/lsif_data_spec.rb
+++ b/spec/requests/api/lsif_data_spec.rb
@@ -61,7 +61,11 @@ describe API::LsifData do
'end_line' => 8,
'start_char' => 13,
'start_line' => 8,
- 'definition_url' => project_blob_path(project, "#{commit.id}/morestrings/reverse.go", anchor: 'L5')
+ 'definition_url' => project_blob_path(project, "#{commit.id}/morestrings/reverse.go", anchor: 'L5'),
+ 'hover' => [{
+ 'language' => 'go',
+ 'value' => Gitlab::Highlight.highlight(nil, 'func Func2(i int) string', language: 'go')
+ }]
})
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 00af0937dd7..58190beb38a 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -267,8 +267,7 @@ describe API::MergeRequests do
it 'returns an array of merge requests with any label when filtering by any label' do
get api(endpoint_path, user), params: { labels: [" #{label.title} ", " #{label2.title} "] }
- expect_successful_response_with_paginated_array
- expect(json_response.length).to eq(1)
+ expect_paginated_array_response([merge_request.id])
expect(json_response.first['labels']).to eq([label2.title, label.title])
expect(json_response.first['id']).to eq(merge_request.id)
end
@@ -276,8 +275,7 @@ describe API::MergeRequests do
it 'returns an array of merge requests with any label when filtering by any label' do
get api(endpoint_path, user), params: { labels: ["#{label.title} , #{label2.title}"] }
- expect_successful_response_with_paginated_array
- expect(json_response.length).to eq(1)
+ expect_paginated_array_response([merge_request.id])
expect(json_response.first['labels']).to eq([label2.title, label.title])
expect(json_response.first['id']).to eq(merge_request.id)
end
@@ -285,17 +283,16 @@ describe API::MergeRequests do
it 'returns an array of merge requests with any label when filtering by any label' do
get api(endpoint_path, user), params: { labels: IssuesFinder::FILTER_ANY }
- expect_successful_response_with_paginated_array
- expect(json_response.length).to eq(1)
+ expect_paginated_array_response([merge_request.id])
expect(json_response.first['id']).to eq(merge_request.id)
end
it 'returns an array of merge requests without a label when filtering by no label' do
get api(endpoint_path, user), params: { labels: IssuesFinder::FILTER_NONE }
- expect_paginated_array_response([
+ expect_response_contain_exactly(
merge_request_merged.id, merge_request_locked.id, merge_request_closed.id
- ])
+ )
end
end
@@ -315,7 +312,7 @@ describe API::MergeRequests do
get api(path, user)
- expect_paginated_array_response([mr2.id])
+ expect_response_contain_exactly(mr2.id)
end
context 'with ordering' do
@@ -413,9 +410,9 @@ describe API::MergeRequests do
it 'returns merge requests with the given source branch' do
get api(endpoint_path, user), params: { source_branch: merge_request_closed.source_branch, state: 'all' }
- expect_paginated_array_response([
+ expect_response_contain_exactly(
merge_request_merged.id, merge_request_locked.id, merge_request_closed.id
- ])
+ )
end
end
@@ -423,9 +420,9 @@ describe API::MergeRequests do
it 'returns merge requests with the given target branch' do
get api(endpoint_path, user), params: { target_branch: merge_request_closed.target_branch, state: 'all' }
- expect_paginated_array_response([
+ expect_response_contain_exactly(
merge_request_merged.id, merge_request_locked.id, merge_request_closed.id
- ])
+ )
end
end
end
@@ -448,10 +445,10 @@ describe API::MergeRequests do
it 'returns an array of all merge requests' do
get api('/merge_requests', user), params: { scope: 'all' }
- expect_paginated_array_response([
+ expect_response_contain_exactly(
merge_request_merged.id, merge_request_locked.id,
merge_request_closed.id, merge_request.id
- ])
+ )
end
it "returns authentication error without any scope" do
@@ -487,9 +484,10 @@ describe API::MergeRequests do
it 'returns an array of all merge requests except unauthorized ones' do
get api('/merge_requests', user), params: { scope: :all }
- expect_paginated_array_response([
- merge_request_merged.id, merge_request2.id, merge_request_locked.id, merge_request_closed.id, merge_request.id
- ])
+ expect_response_contain_exactly(
+ merge_request_merged.id, merge_request2.id, merge_request_locked.id,
+ merge_request_closed.id, merge_request.id
+ )
end
it "returns an array of no merge_requests when wip=yes" do
@@ -501,9 +499,10 @@ describe API::MergeRequests do
it "returns an array of no merge_requests when wip=no" do
get api("/merge_requests", user), params: { wip: 'no' }
- expect_paginated_array_response([
- merge_request_merged.id, merge_request2.id, merge_request_locked.id, merge_request_closed.id, merge_request.id
- ])
+ expect_response_contain_exactly(
+ merge_request_merged.id, merge_request2.id, merge_request_locked.id,
+ merge_request_closed.id, merge_request.id
+ )
end
it 'does not return unauthorized merge requests' do
@@ -512,9 +511,10 @@ describe API::MergeRequests do
get api('/merge_requests', user), params: { scope: :all }
- expect_paginated_array_response([
- merge_request_merged.id, merge_request2.id, merge_request_locked.id, merge_request_closed.id, merge_request.id
- ])
+ expect_response_contain_exactly(
+ merge_request_merged.id, merge_request2.id, merge_request_locked.id,
+ merge_request_closed.id, merge_request.id
+ )
expect(json_response.map { |mr| mr['id'] }).not_to include(merge_request3.id)
end
@@ -523,7 +523,7 @@ describe API::MergeRequests do
get api('/merge_requests', user2)
- expect_paginated_array_response([merge_request3.id])
+ expect_response_contain_exactly(merge_request3.id)
end
it 'returns an array of merge requests authored by the given user' do
@@ -531,7 +531,7 @@ describe API::MergeRequests do
get api('/merge_requests', user), params: { author_id: user2.id, scope: :all }
- expect_paginated_array_response([merge_request3.id])
+ expect_response_contain_exactly(merge_request3.id)
end
it 'returns an array of merge requests assigned to the given user' do
@@ -539,7 +539,7 @@ describe API::MergeRequests do
get api('/merge_requests', user), params: { assignee_id: user2.id, scope: :all }
- expect_paginated_array_response([merge_request3.id])
+ expect_response_contain_exactly(merge_request3.id)
end
it 'returns an array of merge requests with no assignee' do
@@ -547,7 +547,7 @@ describe API::MergeRequests do
get api('/merge_requests', user), params: { assignee_id: 'None', scope: :all }
- expect_paginated_array_response([merge_request3.id])
+ expect_response_contain_exactly(merge_request3.id)
end
it 'returns an array of merge requests with any assignee' do
@@ -556,10 +556,10 @@ describe API::MergeRequests do
get api('/merge_requests', user), params: { assignee_id: 'Any', scope: :all }
- expect_paginated_array_response([
+ expect_response_contain_exactly(
merge_request_merged.id, merge_request2.id, merge_request_locked.id,
merge_request_closed.id, merge_request.id
- ])
+ )
end
it 'returns an array of merge requests assigned to me' do
@@ -567,7 +567,7 @@ describe API::MergeRequests do
get api('/merge_requests', user2), params: { scope: 'assigned_to_me' }
- expect_paginated_array_response([merge_request3.id])
+ expect_response_contain_exactly(merge_request3.id)
end
it 'returns an array of merge requests assigned to me (kebab-case)' do
@@ -575,7 +575,7 @@ describe API::MergeRequests do
get api('/merge_requests', user2), params: { scope: 'assigned-to-me' }
- expect_paginated_array_response([merge_request3.id])
+ expect_response_contain_exactly(merge_request3.id)
end
it 'returns an array of merge requests created by me' do
@@ -583,7 +583,7 @@ describe API::MergeRequests do
get api('/merge_requests', user2), params: { scope: 'created_by_me' }
- expect_paginated_array_response([merge_request3.id])
+ expect_response_contain_exactly(merge_request3.id)
end
it 'returns an array of merge requests created by me (kebab-case)' do
@@ -591,7 +591,7 @@ describe API::MergeRequests do
get api('/merge_requests', user2), params: { scope: 'created-by-me' }
- expect_paginated_array_response([merge_request3.id])
+ expect_response_contain_exactly(merge_request3.id)
end
it 'returns merge requests reacted by the authenticated user by the given emoji' do
@@ -600,16 +600,16 @@ describe API::MergeRequests do
get api('/merge_requests', user2), params: { my_reaction_emoji: award_emoji.name, scope: 'all' }
- expect_paginated_array_response([merge_request3.id])
+ expect_response_contain_exactly(merge_request3.id)
end
context 'source_branch param' do
it 'returns merge requests with the given source branch' do
get api('/merge_requests', user), params: { source_branch: merge_request_closed.source_branch, state: 'all' }
- expect_paginated_array_response([
+ expect_response_contain_exactly(
merge_request_merged.id, merge_request_locked.id, merge_request_closed.id
- ])
+ )
end
end
@@ -617,9 +617,9 @@ describe API::MergeRequests do
it 'returns merge requests with the given target branch' do
get api('/merge_requests', user), params: { target_branch: merge_request_closed.target_branch, state: 'all' }
- expect_paginated_array_response([
+ expect_response_contain_exactly(
merge_request_merged.id, merge_request_locked.id, merge_request_closed.id
- ])
+ )
end
end
@@ -628,7 +628,7 @@ describe API::MergeRequests do
get api('/merge_requests?created_before=2000-01-02T00:00:00.060Z', user)
- expect_paginated_array_response([merge_request2.id])
+ expect_response_contain_exactly(merge_request2.id)
end
it 'returns merge requests created after a specific date' do
@@ -636,7 +636,7 @@ describe API::MergeRequests do
get api("/merge_requests?created_after=#{merge_request2.created_at}", user)
- expect_paginated_array_response([merge_request2.id])
+ expect_response_contain_exactly(merge_request2.id)
end
it 'returns merge requests updated before a specific date' do
@@ -644,7 +644,7 @@ describe API::MergeRequests do
get api('/merge_requests?updated_before=2000-01-02T00:00:00.060Z', user)
- expect_paginated_array_response([merge_request2.id])
+ expect_response_contain_exactly(merge_request2.id)
end
it 'returns merge requests updated after a specific date' do
@@ -652,7 +652,7 @@ describe API::MergeRequests do
get api("/merge_requests?updated_after=#{merge_request2.updated_at}", user)
- expect_paginated_array_response([merge_request2.id])
+ expect_response_contain_exactly(merge_request2.id)
end
context 'search params' do
@@ -663,13 +663,13 @@ describe API::MergeRequests do
it 'returns merge requests matching given search string for title' do
get api("/merge_requests", user), params: { search: merge_request.title }
- expect_paginated_array_response([merge_request.id])
+ expect_response_contain_exactly(merge_request.id)
end
it 'returns merge requests matching given search string for title and scoped in title' do
get api("/merge_requests", user), params: { search: merge_request.title, in: 'title' }
- expect_paginated_array_response([merge_request.id])
+ expect_response_contain_exactly(merge_request.id)
end
it 'returns an empty array if no merge request matches given search string for description and scoped in title' do
@@ -681,7 +681,7 @@ describe API::MergeRequests do
it 'returns merge requests for project matching given search string for description' do
get api("/merge_requests", user), params: { project_id: project.id, search: merge_request.description }
- expect_paginated_array_response([merge_request.id])
+ expect_response_contain_exactly(merge_request.id)
end
end
@@ -689,7 +689,7 @@ describe API::MergeRequests do
it 'returns merge requests with the given state' do
get api('/merge_requests', user), params: { state: 'locked' }
- expect_paginated_array_response([merge_request_locked.id])
+ expect_response_contain_exactly(merge_request_locked.id)
end
end
end
@@ -792,10 +792,10 @@ describe API::MergeRequests do
it 'returns an array excluding merge_requests from archived projects' do
get api(endpoint_path, user)
- expect_paginated_array_response([
+ expect_response_contain_exactly(
merge_request_merged.id, merge_request_locked.id,
merge_request_closed.id, merge_request.id
- ])
+ )
end
context 'with non_archived param set as false' do
@@ -804,10 +804,10 @@ describe API::MergeRequests do
get api(path, user)
- expect_paginated_array_response([
+ expect_response_contain_exactly(
merge_request_merged.id, merge_request_archived.id, merge_request_locked.id,
merge_request_closed.id, merge_request.id
- ])
+ )
end
end
end
diff --git a/spec/requests/self_monitoring_project_spec.rb b/spec/requests/self_monitoring_project_spec.rb
index 5e46645e7a0..6a0258c349f 100644
--- a/spec/requests/self_monitoring_project_spec.rb
+++ b/spec/requests/self_monitoring_project_spec.rb
@@ -17,11 +17,7 @@ describe 'Self-Monitoring project requests' do
login_as(admin)
end
- context 'with feature flag disabled' do
- it_behaves_like 'not accessible if feature flag is disabled'
- end
-
- context 'with feature flag enabled' do
+ context 'when the self monitoring project is created' do
let(:status_api) { status_create_self_monitoring_project_admin_application_settings_path }
it_behaves_like 'triggers async worker, returns sidekiq job_id with response accepted'
@@ -45,11 +41,7 @@ describe 'Self-Monitoring project requests' do
login_as(admin)
end
- context 'with feature flag disabled' do
- it_behaves_like 'not accessible if feature flag is disabled'
- end
-
- context 'with feature flag enabled' do
+ context 'when the self monitoring project is being created' do
it_behaves_like 'handles invalid job_id'
context 'when job is in progress' do
@@ -129,11 +121,7 @@ describe 'Self-Monitoring project requests' do
login_as(admin)
end
- context 'with feature flag disabled' do
- it_behaves_like 'not accessible if feature flag is disabled'
- end
-
- context 'with feature flag enabled' do
+ context 'when the self monitoring project is deleted' do
let(:status_api) { status_delete_self_monitoring_project_admin_application_settings_path }
it_behaves_like 'triggers async worker, returns sidekiq job_id with response accepted'
@@ -157,11 +145,7 @@ describe 'Self-Monitoring project requests' do
login_as(admin)
end
- context 'with feature flag disabled' do
- it_behaves_like 'not accessible if feature flag is disabled'
- end
-
- context 'with feature flag enabled' do
+ context 'when the self monitoring project is being deleted' do
it_behaves_like 'handles invalid job_id'
context 'when job is in progress' do
diff --git a/spec/services/projects/lsif_data_service_spec.rb b/spec/services/projects/lsif_data_service_spec.rb
index 29a99a96c41..93579869d1d 100644
--- a/spec/services/projects/lsif_data_service_spec.rb
+++ b/spec/services/projects/lsif_data_service_spec.rb
@@ -12,6 +12,10 @@ describe Projects::LsifDataService do
let(:service) { described_class.new(artifact.file, project, params) }
describe '#execute' do
+ def highlighted_value(value)
+ [{ language: 'go', value: Gitlab::Highlight.highlight(nil, value, language: 'go') }]
+ end
+
context 'fetched lsif file', :use_clean_rails_memory_store_caching do
it 'is cached' do
service.execute
@@ -32,42 +36,48 @@ describe Projects::LsifDataService do
end_line: 6,
start_char: 5,
start_line: 6,
- definition_url: "#{path_prefix}/main.go#L7"
+ definition_url: "#{path_prefix}/main.go#L7",
+ hover: highlighted_value('func main()')
},
{
end_char: 36,
end_line: 3,
start_char: 1,
start_line: 3,
- definition_url: "#{path_prefix}/main.go#L4"
+ definition_url: "#{path_prefix}/main.go#L4",
+ hover: highlighted_value('package "github.com/user/hello/morestrings" ("github.com/user/hello/morestrings")')
},
{
end_char: 12,
end_line: 7,
start_char: 1,
start_line: 7,
- definition_url: "#{path_prefix}/main.go#L4"
+ definition_url: "#{path_prefix}/main.go#L4",
+ hover: highlighted_value('package "github.com/user/hello/morestrings" ("github.com/user/hello/morestrings")')
},
{
end_char: 20,
end_line: 7,
start_char: 13,
start_line: 7,
- definition_url: "#{path_prefix}/morestrings/reverse.go#L11"
+ definition_url: "#{path_prefix}/morestrings/reverse.go#L11",
+ hover: highlighted_value('func Reverse(s string) string') + [{ value: "This method reverses a string \n\n" }]
},
{
end_char: 12,
end_line: 8,
start_char: 1,
start_line: 8,
- definition_url: "#{path_prefix}/main.go#L4"
+ definition_url: "#{path_prefix}/main.go#L4",
+ hover: highlighted_value('package "github.com/user/hello/morestrings" ("github.com/user/hello/morestrings")')
},
{
end_char: 18,
end_line: 8,
start_char: 13,
start_line: 8,
- definition_url: "#{path_prefix}/morestrings/reverse.go#L5"
+ definition_url: "#{path_prefix}/morestrings/reverse.go#L5",
+ hover: highlighted_value('func Func2(i int) string')
}
])
end
@@ -82,7 +92,8 @@ describe Projects::LsifDataService do
end_line: 11,
start_char: 1,
start_line: 11,
- definition_url: "/#{project.full_path}/-/blob/#{commit_id}/morestrings/reverse.go#L12"
+ definition_url: "/#{project.full_path}/-/blob/#{commit_id}/morestrings/reverse.go#L12",
+ hover: highlighted_value('var a string')
})
end
end
diff --git a/spec/support/helpers/api_helpers.rb b/spec/support/helpers/api_helpers.rb
index 44c38df71b0..dc263d64bcc 100644
--- a/spec/support/helpers/api_helpers.rb
+++ b/spec/support/helpers/api_helpers.rb
@@ -58,6 +58,13 @@ module ApiHelpers
expect(json_response.map { |item| item['id'] }).to eq(Array(items))
end
+ def expect_response_contain_exactly(*items)
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(items.size)
+ expect(json_response.map { |item| item['id'] }).to contain_exactly(*items)
+ end
+
def stub_last_activity_update
allow_any_instance_of(Users::ActivityService).to receive(:execute)
end
diff --git a/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb b/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb
index f6cb2555450..db11b1fe07d 100644
--- a/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb
+++ b/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb
@@ -1,23 +1,5 @@
# frozen_string_literal: true
-RSpec.shared_examples 'not accessible if feature flag is disabled' do
- before do
- stub_feature_flags(self_monitoring_project: false)
- end
-
- it 'returns not_implemented' do
- subject
-
- aggregate_failures do
- expect(response).to have_gitlab_http_status(:not_implemented)
- expect(json_response).to eq(
- 'message' => _('Self-monitoring is not enabled on this GitLab server, contact your administrator.'),
- 'documentation_url' => help_page_path('administration/monitoring/gitlab_self_monitoring_project/index')
- )
- end
- end
-end
-
RSpec.shared_examples 'not accessible to non-admin users' do
context 'with unauthenticated user' do
it 'redirects to signin page' do
diff --git a/spec/views/projects/tree/_tree_header.html.haml_spec.rb b/spec/views/projects/tree/_tree_header.html.haml_spec.rb
index caf8c4d1969..69ad331f880 100644
--- a/spec/views/projects/tree/_tree_header.html.haml_spec.rb
+++ b/spec/views/projects/tree/_tree_header.html.haml_spec.rb
@@ -19,12 +19,12 @@ describe 'projects/tree/_tree_header' do
allow(view).to receive(:can_collaborate_with_project?) { true }
end
- it 'does not render the WebIDE button when user cannot create fork or cannot open MR' do
+ it 'renders the WebIDE button when user can collaborate but not create fork or MR' do
allow(view).to receive(:can?) { false }
render
- expect(rendered).not_to have_link('Web IDE')
+ expect(rendered).to have_link('Web IDE')
end
it 'renders the WebIDE button when user can create fork and can open MR in project' do
@@ -43,4 +43,13 @@ describe 'projects/tree/_tree_header' do
expect(rendered).to have_link('Web IDE', href: '#modal-confirm-fork')
end
+
+ it 'does not render the WebIDE button when user cannot collaborate or create mr' do
+ allow(view).to receive(:can?) { false }
+ allow(view).to receive(:can_collaborate_with_project?) { false }
+
+ render
+
+ expect(rendered).not_to have_link('Web IDE')
+ end
end