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--Gemfile2
-rw-r--r--Gemfile.checksum3
-rw-r--r--Gemfile.lock6
-rw-r--r--app/assets/javascripts/api/projects_api.js7
-rw-r--r--app/assets/javascripts/groups/settings/components/group_settings_readme.vue147
-rw-r--r--app/assets/javascripts/groups/settings/constants.js4
-rw-r--r--app/assets/javascripts/groups/settings/init_group_settings_readme.js24
-rw-r--r--app/assets/javascripts/lib/utils/web_ide_navigator.js24
-rw-r--r--app/assets/javascripts/pages/groups/edit/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js2
-rw-r--r--app/assets/javascripts/repository/components/fork_info.vue46
-rw-r--r--app/assets/javascripts/repository/index.js6
-rw-r--r--app/assets/javascripts/super_sidebar/components/merge_request_menu.vue7
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_bar.vue9
-rw-r--r--app/assets/stylesheets/components/whats_new.scss14
-rw-r--r--app/finders/groups/accepting_project_creations_finder.rb105
-rw-r--r--app/finders/groups/user_groups_finder.rb2
-rw-r--r--app/helpers/groups_helper.rb9
-rw-r--r--app/helpers/projects_helper.rb9
-rw-r--r--app/models/concerns/expirable.rb2
-rw-r--r--app/models/group.rb6
-rw-r--r--app/models/group_group_link.rb8
-rw-r--r--app/views/groups/settings/_general.html.haml6
-rw-r--r--config/feature_flags/development/include_groups_from_group_shares_in_project_creation_locations.yml8
-rw-r--r--config/vue3migration/compiler.js15
-rw-r--r--doc/administration/audit_events.md36
-rw-r--r--doc/api/keys.md14
-rw-r--r--doc/api/project_import_export.md107
-rw-r--r--doc/api/repositories.md37
-rw-r--r--doc/ci/environments/external_deployment_tools.md2
-rw-r--r--doc/development/graphql_guide/reviewing.md2
-rw-r--r--doc/user/group/saml_sso/index.md15
-rw-r--r--doc/user/project/merge_requests/index.md10
-rw-r--r--locale/gitlab.pot21
-rw-r--r--spec/features/groups/group_settings_spec.rb73
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb2
-rw-r--r--spec/finders/groups/accepting_project_creations_finder_spec.rb119
-rw-r--r--spec/frontend/api/projects_api_spec.js14
-rw-r--r--spec/frontend/groups/settings/components/group_settings_readme_spec.js112
-rw-r--r--spec/frontend/groups/settings/mock_data.js6
-rw-r--r--spec/frontend/lib/utils/web_ide_navigator_spec.js38
-rw-r--r--spec/frontend/repository/components/fork_info_spec.js39
-rw-r--r--spec/frontend/repository/mock_data.js2
-rw-r--r--spec/helpers/projects_helper_spec.rb6
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml3
-rw-r--r--spec/models/concerns/expirable_spec.rb66
-rw-r--r--spec/models/group_group_link_spec.rb128
-rw-r--r--spec/models/group_spec.rb17
-rw-r--r--spec/views/groups/settings/_general.html.haml_spec.rb39
49 files changed, 1145 insertions, 237 deletions
diff --git a/Gemfile b/Gemfile
index e0ba0f8cccf..55d915dbad7 100644
--- a/Gemfile
+++ b/Gemfile
@@ -456,7 +456,7 @@ group :test do
gem 'rspec-benchmark', '~> 0.6.0'
gem 'rspec-parameterized', '~> 1.0', require: false
- gem 'capybara', '~> 3.35.3'
+ gem 'capybara', '~> 3.39'
gem 'capybara-screenshot', '~> 1.0.22'
gem 'selenium-webdriver', '~> 3.142'
diff --git a/Gemfile.checksum b/Gemfile.checksum
index 5bf69f044d0..f3399786244 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -67,7 +67,7 @@
{"name":"bullet","version":"7.0.2","platform":"ruby","checksum":"4b7986b366f694bb05d5c1b4ea8ba949a99224d4511bf02f0c3944112f719c81"},
{"name":"bundler-audit","version":"0.7.0.1","platform":"ruby","checksum":"12d853cb0b92fa8868abbb539414d7a33da9e48b792e2ff28271d36c8ace8912"},
{"name":"byebug","version":"11.1.3","platform":"ruby","checksum":"2485944d2bb21283c593d562f9ae1019bf80002143cc3a255aaffd4e9cf4a35b"},
-{"name":"capybara","version":"3.35.3","platform":"ruby","checksum":"3389f8203b05175352b763f4d04c31b29ba606a96224649ac42ef967f56538ee"},
+{"name":"capybara","version":"3.39.0","platform":"ruby","checksum":"a30994beb4b4f318e39e3dc81e73203bd1edf1f9ef237d82b708eb1c21b56419"},
{"name":"capybara-screenshot","version":"1.0.22","platform":"ruby","checksum":"f86040349a0df7f723123460d9456023f7d693068338991529f10f670fa420f5"},
{"name":"carrierwave","version":"1.3.3","platform":"ruby","checksum":"0f0244de2ece54c80745b755993bd26cf47d4650823e5f89c115dbc9d73a13f1"},
{"name":"cbor","version":"0.5.9.6","platform":"ruby","checksum":"434a147658dd1df24ec9e7b3297c1fd4f8a691c97d0e688b3049df8e728b2114"},
@@ -345,6 +345,7 @@
{"name":"mail","version":"2.8.1","platform":"ruby","checksum":"ec3b9fadcf2b3755c78785cb17bc9a0ca9ee9857108a64b6f5cfc9c0b5bfc9ad"},
{"name":"marcel","version":"1.0.2","platform":"ruby","checksum":"a013b677ef46cbcb49fd5c59b3d35803d2ee04dd75d8bfdc43533fc5a31f7e4e"},
{"name":"marginalia","version":"1.11.1","platform":"ruby","checksum":"cb63212ab63e42746e27595e912cb20408a1a28bcd0edde55d15b7c45fa289cf"},
+{"name":"matrix","version":"0.4.2","platform":"ruby","checksum":"71083ccbd67a14a43bfa78d3e4dc0f4b503b9cc18e5b4b1d686dc0f9ef7c4cc0"},
{"name":"memoist","version":"0.16.2","platform":"ruby","checksum":"a52c53a3f25b5875151670b2f3fd44388633486dc0f09f9a7150ead1e3bf3c45"},
{"name":"memory_profiler","version":"1.0.1","platform":"ruby","checksum":"38cdb42f22d9100df2eba0365c199724b58b05c38e765cd764a07392916901b1"},
{"name":"method_source","version":"1.0.0","platform":"ruby","checksum":"d779455a2b5666a079ce58577bfad8534f571af7cec8107f4dce328f0981dede"},
diff --git a/Gemfile.lock b/Gemfile.lock
index 5d10f5f4655..d953709bde7 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -274,8 +274,9 @@ GEM
bundler (>= 1.2.0, < 3)
thor (>= 0.18, < 2)
byebug (11.1.3)
- capybara (3.35.3)
+ capybara (3.39.0)
addressable
+ matrix
mini_mime (>= 0.1.3)
nokogiri (~> 1.8)
rack (>= 1.6.0)
@@ -946,6 +947,7 @@ GEM
marginalia (1.11.1)
actionpack (>= 5.2)
activerecord (>= 5.2)
+ matrix (0.4.2)
memoist (0.16.2)
memory_profiler (1.0.1)
method_source (1.0.0)
@@ -1684,7 +1686,7 @@ DEPENDENCIES
bullet (~> 7.0.2)
bundler-audit (~> 0.7.0.1)
bundler-checksum (~> 0.1.0)!
- capybara (~> 3.35.3)
+ capybara (~> 3.39)
capybara-screenshot (~> 1.0.22)
carrierwave (~> 1.3)
charlock_holmes (~> 0.7.7)
diff --git a/app/assets/javascripts/api/projects_api.js b/app/assets/javascripts/api/projects_api.js
index 5a794dcd035..c72a913aacd 100644
--- a/app/assets/javascripts/api/projects_api.js
+++ b/app/assets/javascripts/api/projects_api.js
@@ -35,6 +35,13 @@ export function getProjects(query, options, callback = () => {}) {
});
}
+export function createProject(projectData) {
+ const url = buildApiUrl(PROJECTS_PATH);
+ return axios.post(url, projectData).then(({ data }) => {
+ return data;
+ });
+}
+
export function importProjectMembers(sourceId, targetId) {
const url = buildApiUrl(PROJECT_IMPORT_MEMBERS_PATH)
.replace(':id', sourceId)
diff --git a/app/assets/javascripts/groups/settings/components/group_settings_readme.vue b/app/assets/javascripts/groups/settings/components/group_settings_readme.vue
new file mode 100644
index 00000000000..123c7fc58f5
--- /dev/null
+++ b/app/assets/javascripts/groups/settings/components/group_settings_readme.vue
@@ -0,0 +1,147 @@
+<script>
+import { GlButton, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui';
+import { s__, __ } from '~/locale';
+import { createProject } from '~/rest_api';
+import { createAlert } from '~/alert';
+import { openWebIDE } from '~/lib/utils/web_ide_navigator';
+import { README_MODAL_ID, GITLAB_README_PROJECT, README_FILE } from '../constants';
+
+export default {
+ name: 'GroupSettingsReadme',
+ i18n: {
+ readme: __('README'),
+ addReadme: __('Add README'),
+ cancel: __('Cancel'),
+ createProjectAndReadme: s__('Groups|Create and add README'),
+ creatingReadme: s__('Groups|Creating README'),
+ existingProjectNewReadme: s__('Groups|This will create a README.md for project %{path}.'),
+ newProjectAndReadme: s__('Groups|This will create a project %{path} and add a README.md.'),
+ errorCreatingProject: s__('Groups|There was an error creating the Group README.'),
+ },
+ components: {
+ GlButton,
+ GlModal,
+ GlSprintf,
+ },
+ directives: {
+ GlModal: GlModalDirective,
+ },
+ props: {
+ groupReadmePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ readmeProjectPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ groupPath: {
+ type: String,
+ required: true,
+ },
+ groupId: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ creatingReadme: false,
+ };
+ },
+ computed: {
+ hasReadme() {
+ return this.groupReadmePath.length > 0;
+ },
+ hasReadmeProject() {
+ return this.readmeProjectPath.length > 0;
+ },
+ pathToReadmeProject() {
+ return this.hasReadmeProject
+ ? this.readmeProjectPath
+ : `${this.groupPath}/${GITLAB_README_PROJECT}`;
+ },
+ modalBody() {
+ return this.hasReadmeProject
+ ? this.$options.i18n.existingProjectNewReadme
+ : this.$options.i18n.newProjectAndReadme;
+ },
+ modalSubmitButtonText() {
+ return this.hasReadmeProject
+ ? this.$options.i18n.addReadme
+ : this.$options.i18n.createProjectAndReadme;
+ },
+ },
+ methods: {
+ hideModal() {
+ this.$refs.modal.hide();
+ },
+ createReadme() {
+ if (this.hasReadmeProject) {
+ openWebIDE(this.readmeProjectPath, README_FILE);
+ } else {
+ this.createProjectWithReadme();
+ }
+ },
+ createProjectWithReadme() {
+ this.creatingReadme = true;
+
+ const projectData = {
+ name: GITLAB_README_PROJECT,
+ namespace_id: this.groupId,
+ };
+
+ createProject(projectData)
+ .then(({ path_with_namespace: pathWithNamespace }) => {
+ openWebIDE(pathWithNamespace, README_FILE);
+ })
+ .catch(() => {
+ this.hideModal();
+ this.creatingReadme = false;
+ createAlert({ message: this.$options.i18n.errorCreatingProject });
+ });
+ },
+ },
+ README_MODAL_ID,
+};
+</script>
+
+<template>
+ <div>
+ <gl-button v-if="hasReadme" icon="doc-text" :href="groupReadmePath">{{
+ $options.i18n.readme
+ }}</gl-button>
+ <gl-button
+ v-else
+ v-gl-modal="$options.README_MODAL_ID"
+ variant="dashed"
+ icon="file-addition"
+ data-testid="group-settings-add-readme-button"
+ >{{ $options.i18n.addReadme }}</gl-button
+ >
+ <gl-modal ref="modal" :modal-id="$options.README_MODAL_ID" :title="$options.i18n.addReadme">
+ <div data-testid="group-settings-modal-readme-body">
+ <gl-sprintf :message="modalBody">
+ <template #path>
+ <code>{{ pathToReadmeProject }}</code>
+ </template>
+ </gl-sprintf>
+ </div>
+ <template #modal-footer>
+ <gl-button variant="default" @click="hideModal">{{ $options.i18n.cancel }}</gl-button>
+ <gl-button v-if="creatingReadme" variant="default" loading disabled>{{
+ $options.i18n.creatingReadme
+ }}</gl-button>
+ <gl-button
+ v-else
+ variant="confirm"
+ data-testid="group-settings-modal-create-readme-button"
+ @click="createReadme"
+ >{{ modalSubmitButtonText }}</gl-button
+ >
+ </template>
+ </gl-modal>
+ </div>
+</template>
diff --git a/app/assets/javascripts/groups/settings/constants.js b/app/assets/javascripts/groups/settings/constants.js
index c91c2a20529..023ddf29b36 100644
--- a/app/assets/javascripts/groups/settings/constants.js
+++ b/app/assets/javascripts/groups/settings/constants.js
@@ -1,3 +1,7 @@
export const LEVEL_TYPES = {
GROUP: 'group',
};
+
+export const README_MODAL_ID = 'add_group_readme_modal';
+export const GITLAB_README_PROJECT = 'gitlab-profile';
+export const README_FILE = 'README.md';
diff --git a/app/assets/javascripts/groups/settings/init_group_settings_readme.js b/app/assets/javascripts/groups/settings/init_group_settings_readme.js
new file mode 100644
index 00000000000..d126228d854
--- /dev/null
+++ b/app/assets/javascripts/groups/settings/init_group_settings_readme.js
@@ -0,0 +1,24 @@
+import Vue from 'vue';
+import GroupSettingsReadme from './components/group_settings_readme.vue';
+
+export const initGroupSettingsReadme = () => {
+ const el = document.getElementById('js-group-settings-readme');
+
+ if (!el) return false;
+
+ const { groupReadmePath, readmeProjectPath, groupPath, groupId } = el.dataset;
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(GroupSettingsReadme, {
+ props: {
+ groupReadmePath,
+ readmeProjectPath,
+ groupPath,
+ groupId,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/lib/utils/web_ide_navigator.js b/app/assets/javascripts/lib/utils/web_ide_navigator.js
new file mode 100644
index 00000000000..f0579b5886d
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/web_ide_navigator.js
@@ -0,0 +1,24 @@
+import { visitUrl, webIDEUrl } from '~/lib/utils/url_utility';
+
+/**
+ * Takes a project path and optional file path and branch
+ * and then redirects the user to the web IDE.
+ *
+ * @param {string} projectPath - Full path to project including namespace (ex. flightjs/Flight)
+ * @param {string} filePath - optional path to file to be edited, otherwise will open at base directory (ex. README.md)
+ * @param {string} branch - optional branch to open the IDE, defaults to 'main'
+ */
+
+export const openWebIDE = (projectPath, filePath, branch = 'main') => {
+ if (!projectPath) {
+ throw new TypeError('projectPath parameter is required');
+ }
+
+ const pathnameSegments = [projectPath, 'edit', branch, '-'];
+
+ if (filePath) {
+ pathnameSegments.push(filePath);
+ }
+
+ visitUrl(webIDEUrl(`/${pathnameSegments.join('/')}/`));
+};
diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js
index dec06fe6f4d..721168f6140 100644
--- a/app/assets/javascripts/pages/groups/edit/index.js
+++ b/app/assets/javascripts/pages/groups/edit/index.js
@@ -9,6 +9,7 @@ import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
import initSearchSettings from '~/search_settings';
import initSettingsPanels from '~/settings_panels';
import initConfirmDanger from '~/init_confirm_danger';
+import { initGroupSettingsReadme } from '~/groups/settings/init_group_settings_readme';
initFilePickers();
initConfirmDanger();
@@ -27,3 +28,5 @@ initProjectSelects();
initSearchSettings();
initCascadingSettingsLockPopovers();
+
+initGroupSettingsReadme();
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index dd39fb7c666..2f8c2a8e86f 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -102,6 +102,7 @@ const initForkInfo = () => {
sourceDefaultBranch,
aheadComparePath,
behindComparePath,
+ canUserCreateMrInFork,
} = forkEl.dataset;
return new Vue({
el: forkEl,
@@ -116,6 +117,7 @@ const initForkInfo = () => {
sourceDefaultBranch,
aheadComparePath,
behindComparePath,
+ canUserCreateMrInFork,
},
});
},
diff --git a/app/assets/javascripts/repository/components/fork_info.vue b/app/assets/javascripts/repository/components/fork_info.vue
index d84e197714e..a7795c8da0a 100644
--- a/app/assets/javascripts/repository/components/fork_info.vue
+++ b/app/assets/javascripts/repository/components/fork_info.vue
@@ -24,7 +24,8 @@ export const i18n = {
behindAhead: s__('ForksDivergence|%{messages} the upstream repository.'),
limitedVisibility: s__('ForksDivergence|Source project has a limited visibility.'),
error: s__('ForksDivergence|Failed to fetch fork details. Try again later.'),
- sync: s__('ForksDivergence|Update fork'),
+ updateFork: s__('ForksDivergence|Update fork'),
+ createMergeRequest: s__('ForksDivergence|Create merge request'),
};
export default {
@@ -103,6 +104,16 @@ export default {
required: false,
default: '',
},
+ createMrPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ canUserCreateMrInFork: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -173,12 +184,15 @@ export default {
hasBehindAheadMessage() {
return this.behindAheadMessage.length > 0;
},
- isSyncButtonAvailable() {
+ hasUpdateButton() {
return (
this.glFeatures.synchronizeFork &&
((this.sourceName && this.forkDetails && this.behind) || this.isUnknownDivergence)
);
},
+ hasCreateMrButton() {
+ return this.canUserCreateMrInFork && this.ahead && this.createMrPath;
+ },
forkDivergenceMessage() {
if (!this.forkDetails) {
return this.$options.i18n.limitedVisibility;
@@ -286,14 +300,26 @@ export default {
>
{{ $options.i18n.inaccessibleProject }}
</div>
- <gl-button
- v-if="isSyncButtonAvailable"
- :disabled="forkDetails.isSyncing"
- @click="checkIfSyncIsPossible"
- >
- <gl-loading-icon v-if="forkDetails.isSyncing" class="gl-display-inline" size="sm" />
- <span>{{ $options.i18n.sync }}</span>
- </gl-button>
+ <div class="gl-display-flex gl-xs-display-none!">
+ <gl-button
+ v-if="hasCreateMrButton"
+ class="gl-ml-4"
+ :href="createMrPath"
+ data-testid="create-mr-button"
+ >
+ <span>{{ $options.i18n.createMergeRequest }}</span>
+ </gl-button>
+ <gl-button
+ v-if="hasUpdateButton"
+ class="gl-ml-4"
+ :disabled="forkDetails.isSyncing"
+ data-testid="update-fork-button"
+ @click="checkIfSyncIsPossible"
+ >
+ <gl-loading-icon v-if="forkDetails.isSyncing" class="gl-display-inline" size="sm" />
+ <span>{{ $options.i18n.updateFork }}</span>
+ </gl-button>
+ </div>
<conflicts-modal
ref="modal"
:source-name="sourceName"
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index 8dc67b97a60..b1217881bc3 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -74,8 +74,10 @@ export default function setupVueRepositoryList() {
sourceName,
sourcePath,
sourceDefaultBranch,
+ createMrPath,
aheadComparePath,
behindComparePath,
+ canUserCreateMrInFork,
} = forkEl.dataset;
return new Vue({
el: forkEl,
@@ -90,6 +92,8 @@ export default function setupVueRepositoryList() {
sourceDefaultBranch,
aheadComparePath,
behindComparePath,
+ createMrPath,
+ canUserCreateMrInFork,
},
});
},
@@ -153,8 +157,8 @@ export default function setupVueRepositoryList() {
initLastCommitApp();
initBlobControlsApp();
- initForkInfo();
initRefSwitcher();
+ initForkInfo();
router.afterEach(({ params: { path } }) => {
setTitle(path, ref, fullName);
diff --git a/app/assets/javascripts/super_sidebar/components/merge_request_menu.vue b/app/assets/javascripts/super_sidebar/components/merge_request_menu.vue
index 94fc6aedcc0..d37e863bed9 100644
--- a/app/assets/javascripts/super_sidebar/components/merge_request_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/merge_request_menu.vue
@@ -16,7 +16,12 @@ export default {
</script>
<template>
- <gl-disclosure-dropdown :items="items" placement="center">
+ <gl-disclosure-dropdown
+ :items="items"
+ placement="center"
+ @shown="$emit('shown')"
+ @hidden="$emit('hidden')"
+ >
<template #toggle>
<slot></slot>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/user_bar.vue b/app/assets/javascripts/super_sidebar/components/user_bar.vue
index e03c587567e..498c082ddb2 100644
--- a/app/assets/javascripts/super_sidebar/components/user_bar.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_bar.vue
@@ -56,6 +56,11 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ mrMenuShown: false,
+ };
+ },
methods: {
collapseSidebar() {
toggleSuperSidebarCollapsed(true, true, true);
@@ -144,9 +149,11 @@ export default {
<merge-request-menu
class="gl-flex-basis-third gl-display-block!"
:items="sidebarData.merge_request_menu"
+ @shown="mrMenuShown = true"
+ @hidden="mrMenuShown = false"
>
<counter
- v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.mergeRequests"
+ v-gl-tooltip:super-sidebar.hover.bottom="mrMenuShown ? '' : $options.i18n.mergeRequests"
class="gl-w-full"
icon="merge-request-open"
:count="sidebarData.total_merge_requests_count"
diff --git a/app/assets/stylesheets/components/whats_new.scss b/app/assets/stylesheets/components/whats_new.scss
index c1c68f64d86..35c619a2e2f 100644
--- a/app/assets/stylesheets/components/whats_new.scss
+++ b/app/assets/stylesheets/components/whats_new.scss
@@ -1,5 +1,5 @@
.whats-new-drawer {
- margin-top: $header-height;
+ margin-top: calc(#{$header-height} + #{$calc-application-bars-height});
@include gl-shadow-none;
overflow-y: hidden;
width: 500px;
@@ -35,18 +35,6 @@
}
}
-.with-performance-bar .whats-new-drawer {
- margin-top: calc(#{$performance-bar-height} + #{$header-height});
-}
-
-.with-system-header .whats-new-drawer {
- margin-top: calc(#{$system-header-height} + #{$header-height});
-}
-
-.with-performance-bar.with-system-header .whats-new-drawer {
- margin-top: calc(#{$performance-bar-height} + #{$system-header-height} + #{$header-height});
-}
-
.whats-new-item-title-link {
&:hover,
&:focus,
diff --git a/app/finders/groups/accepting_project_creations_finder.rb b/app/finders/groups/accepting_project_creations_finder.rb
new file mode 100644
index 00000000000..a7057b3f672
--- /dev/null
+++ b/app/finders/groups/accepting_project_creations_finder.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+module Groups
+ class AcceptingProjectCreationsFinder
+ def initialize(current_user)
+ @current_user = current_user
+ end
+
+ def execute
+ if Feature.disabled?(:include_groups_from_group_shares_in_project_creation_locations)
+ return current_user.manageable_groups(include_groups_with_developer_maintainer_access: true)
+ end
+
+ groups_accepting_project_creations =
+ [
+ current_user
+ .manageable_groups(include_groups_with_developer_maintainer_access: true)
+ .project_creation_allowed,
+ owner_maintainer_groups_originating_from_group_shares
+ .project_creation_allowed,
+ *developer_groups_originating_from_group_shares
+ ]
+
+ # We move the UNION query into a materialized CTE to improve query performance during text search.
+ union_query = ::Group.from_union(groups_accepting_project_creations)
+ cte = Gitlab::SQL::CTE.new(:my_union_cte, union_query)
+
+ Group.with(cte.to_arel).from(cte.alias_to(Group.arel_table)) # rubocop: disable CodeReuse/ActiveRecord
+ end
+
+ private
+
+ attr_reader :current_user
+
+ def owner_maintainer_groups_originating_from_group_shares
+ GroupGroupLink
+ .with_owner_or_maintainer_access
+ .groups_accessible_via(
+ groups_that_user_has_owner_or_maintainer_access_via_direct_membership
+ .select(:id)
+ )
+ end
+
+ def groups_that_user_has_owner_or_maintainer_access_via_direct_membership
+ current_user.owned_or_maintainers_groups
+ end
+
+ def developer_groups_originating_from_group_shares
+ # Example:
+ #
+ # Group A -----shared to---> Group B
+ #
+
+ # Now, there are 2 ways a user in Group A can get "Developer" access to Group B (and it's subgroups)
+ [
+ # 1. User has Developer or above access in Group A,
+ # but the group_group_link has MAX access level set to Developer
+ GroupGroupLink
+ .with_developer_access
+ .groups_accessible_via(
+ groups_that_user_has_developer_access_and_above_via_direct_membership
+ .select(:id)
+ ).with_project_creation_levels(project_creations_levels_allowing_developers_to_create_projects),
+
+ # 2. User has exactly Developer access in Group A,
+ # but the group_group_link has MAX access level set to Developer or above.
+ GroupGroupLink
+ .with_developer_maintainer_owner_access
+ .groups_accessible_via(
+ groups_that_user_has_developer_access_via_direct_membership
+ .select(:id)
+ ).with_project_creation_levels(project_creations_levels_allowing_developers_to_create_projects)
+ ]
+
+ # Lastly, we should make sure that such groups indeed allow Developers to create projects in them,
+ # based on the value of `groups.project_creation_level`,
+ # which is why we use the scope .with_project_creation_levels on each set.
+ end
+
+ def groups_that_user_has_developer_access_and_above_via_direct_membership
+ current_user.developer_maintainer_owned_groups
+ end
+
+ def groups_that_user_has_developer_access_via_direct_membership
+ current_user.developer_groups
+ end
+
+ def project_creations_levels_allowing_developers_to_create_projects
+ project_creation_levels = [::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS]
+
+ # When the value of application_settings.default_project_creation is set to `DEVELOPER_MAINTAINER_PROJECT_ACCESS`,
+ # it means that a `nil` value for `groups.project_creation_level` is telling us:
+ # such groups also have `project_creation_level` implicitly set to `DEVELOPER_MAINTAINER_PROJECT_ACCESS`.
+ # ie, `nil` is a placeholder value for inheriting the value from the ApplicationSetting.
+ # So we will include `nil` in the list,
+ # when the application_setting's value is `DEVELOPER_MAINTAINER_PROJECT_ACCESS`
+
+ if ::Gitlab::CurrentSettings.default_project_creation == ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS
+ project_creation_levels << nil
+ end
+
+ project_creation_levels
+ end
+ end
+end
diff --git a/app/finders/groups/user_groups_finder.rb b/app/finders/groups/user_groups_finder.rb
index b58c1323b1f..83e012b3dbe 100644
--- a/app/finders/groups/user_groups_finder.rb
+++ b/app/finders/groups/user_groups_finder.rb
@@ -36,7 +36,7 @@ module Groups
def by_permission_scope
if permission_scope_create_projects?
- target_user.manageable_groups(include_groups_with_developer_maintainer_access: true)
+ Groups::AcceptingProjectCreationsFinder.new(target_user).execute # rubocop: disable CodeReuse/Finder
elsif permission_scope_transfer_projects?
Groups::AcceptingProjectTransfersFinder.new(target_user).execute # rubocop: disable CodeReuse/Finder
else
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 85bbc796dab..66e710485af 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -180,6 +180,15 @@ module GroupsHelper
Feature.enabled?(:show_group_readme, group) && group.group_readme
end
+ def group_settings_readme_app_data(group)
+ {
+ group_readme_path: group.group_readme&.present&.web_path,
+ readme_project_path: group.readme_project&.present&.path_with_namespace,
+ group_path: group.full_path,
+ group_id: group.id
+ }
+ end
+
def enabled_git_access_protocol_options_for_group
case ::Gitlab::CurrentSettings.enabled_git_access_protocol
when nil, ""
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 1a0e26bd848..21a736bf68a 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -2,6 +2,7 @@
module ProjectsHelper
include Gitlab::Utils::StrongMemoize
+ include CompareHelper
def project_incident_management_setting
@project_incident_management_setting ||= @project.incident_management_setting ||
@@ -139,9 +140,11 @@ module ProjectsHelper
ahead_compare_path: project_compare_path(
project, from: source_default_branch, to: ref, from_project_id: source_project.id
),
+ create_mr_path: create_mr_path(from: ref, source_project: project, to: source_default_branch, target_project: source_project),
behind_compare_path: project_compare_path(
source_project, from: ref, to: source_default_branch, from_project_id: project.id
- )
+ ),
+ can_user_create_mr_in_fork: can_user_create_mr_in_fork(source_project)
}
end
@@ -163,6 +166,10 @@ module ProjectsHelper
project.fork_source if project.fork_source && can?(current_user, :read_project, project.fork_source)
end
+ def can_user_create_mr_in_fork(project)
+ can?(current_user, :create_merge_request_in, project)
+ end
+
def project_search_tabs?(tab)
return false unless @project.present?
diff --git a/app/models/concerns/expirable.rb b/app/models/concerns/expirable.rb
index 5975ea23723..cc55315d6d7 100644
--- a/app/models/concerns/expirable.rb
+++ b/app/models/concerns/expirable.rb
@@ -8,7 +8,7 @@ module Expirable
included do
scope :not, ->(scope) { where(scope.arel.constraints.reduce(:and).not) }
- scope :expired, -> { where('expires_at IS NOT NULL AND expires_at <= ?', Time.current) }
+ scope :expired, -> { where.not(expires_at: nil).where(arel_table[:expires_at].lteq(Time.current)) }
scope :not_expired, -> { self.not(expired) }
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 4aa97786ca1..f13ce2ddca1 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -200,6 +200,10 @@ class Group < Namespace
.where(project_authorizations: { user_id: user_ids })
end
+ scope :with_project_creation_levels, -> (project_creation_levels) do
+ where(project_creation_level: project_creation_levels)
+ end
+
scope :project_creation_allowed, -> do
project_creation_allowed_on_levels = [
::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS,
@@ -216,7 +220,7 @@ class Group < Namespace
project_creation_allowed_on_levels.delete(nil)
end
- where(project_creation_level: project_creation_allowed_on_levels)
+ with_project_creation_levels(project_creation_allowed_on_levels)
end
scope :shared_into_ancestors, -> (group) do
diff --git a/app/models/group_group_link.rb b/app/models/group_group_link.rb
index 15949570f9c..fdb8fb9ed75 100644
--- a/app/models/group_group_link.rb
+++ b/app/models/group_group_link.rb
@@ -19,6 +19,14 @@ class GroupGroupLink < ApplicationRecord
where(group_access: [Gitlab::Access::OWNER, Gitlab::Access::MAINTAINER])
end
+ scope :with_developer_maintainer_owner_access, -> do
+ where(group_access: [Gitlab::Access::DEVELOPER, Gitlab::Access::MAINTAINER, Gitlab::Access::OWNER])
+ end
+
+ scope :with_developer_access, -> do
+ where(group_access: [Gitlab::Access::DEVELOPER])
+ end
+
scope :with_owner_access, -> do
where(group_access: [Gitlab::Access::OWNER])
end
diff --git a/app/views/groups/settings/_general.html.haml b/app/views/groups/settings/_general.html.haml
index 5258854c931..dc04a2079c9 100644
--- a/app/views/groups/settings/_general.html.haml
+++ b/app/views/groups/settings/_general.html.haml
@@ -19,6 +19,12 @@
= f.label :description, s_('Groups|Group description (optional)'), class: 'label-bold'
= f.text_area :description, class: 'form-control', rows: 3, maxlength: 250
+ - if Feature.enabled?(:show_group_readme, @group)
+ .row.gl-mt-3
+ .form-group.col-md-5
+ = f.label :description, s_('Groups|Group README'), class: 'label-bold'
+ #js-group-settings-readme{ data: group_settings_readme_app_data(@group) }
+
= render 'shared/repository_size_limit_setting_registration_features_cta', form: f
= render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group
diff --git a/config/feature_flags/development/include_groups_from_group_shares_in_project_creation_locations.yml b/config/feature_flags/development/include_groups_from_group_shares_in_project_creation_locations.yml
new file mode 100644
index 00000000000..7a2770f4b27
--- /dev/null
+++ b/config/feature_flags/development/include_groups_from_group_shares_in_project_creation_locations.yml
@@ -0,0 +1,8 @@
+---
+name: include_groups_from_group_shares_in_project_creation_locations
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/116089
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/403019
+milestone: '15.11'
+type: development
+group: group::tenant scale
+default_enabled: false
diff --git a/config/vue3migration/compiler.js b/config/vue3migration/compiler.js
index bb92e1e2356..a2c82584227 100644
--- a/config/vue3migration/compiler.js
+++ b/config/vue3migration/compiler.js
@@ -1,5 +1,7 @@
const { parse, compile: compilerDomCompile } = require('@vue/compiler-dom');
+const COMMENT_NODE_TYPE = 3;
+
const getPropIndex = (node, prop) => node.props?.findIndex((p) => p.name === prop) ?? -1;
function modifyKeysInsideTemplateTag(templateNode) {
@@ -26,6 +28,19 @@ module.exports = {
parse,
compile(template, options) {
const rootNode = parse(template, options);
+
+ // We do not want to switch to whitespace: collapse mode which is Vue.js 3 default
+ // It will be too devastating to codebase
+
+ // However, without `whitespace: condense` Vue will treat spaces between comments
+ // and nodes itself as text nodes, resulting in multi-root component
+ // For multi-root component passing classes / attributes fallthrough will not work
+
+ // See https://github.com/vuejs/core/issues/7909 for details
+
+ // To fix that we simply drop all component comments only on top-level
+ rootNode.children = rootNode.children.filter((n) => n.type !== COMMENT_NODE_TYPE);
+
const pendingNodes = [rootNode];
while (pendingNodes.length) {
const currentNode = pendingNodes.pop();
diff --git a/doc/administration/audit_events.md b/doc/administration/audit_events.md
index a600a4d7501..f590229f79e 100644
--- a/doc/administration/audit_events.md
+++ b/doc/administration/audit_events.md
@@ -316,12 +316,9 @@ The following actions on projects generate project audit events:
### GitLab agent for Kubernetes events
-The following actions on projects generate agent audit events:
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/382133) in GitLab 15.10.
-- A cluster agent token is created.
- Introduced in GitLab 15.9
-- A cluster agent token is revoked.
- Introduced in GitLab 15.9
+GitLab generates audit events when a cluster agent token is created or revoked.
### Instance events **(PREMIUM SELF)**
@@ -364,23 +361,18 @@ Instance events can also be accessed using the [Instance Audit Events API](../ap
### GitLab Runner events
-The following GitLab Runner actions generate instance audit events:
-
-- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/335509) in GitLab 14.8:
- - Registered instance runner.
- - Registered group runner.
- - Registered project runner.
-- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/355637) in GitLab 15.0. and [deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102579) in GitLab 15.6:
- - Reset instance runner registration token.
- - Reset group runner registration token.
- - Reset project runner registration token.
-- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/349542) in GitLab 14.9.
- - Assigned runner to project.
- - Unassigned runner from project.
-- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/349540) in GitLab 14.9.
- - Unregistered instance runner.
- - Unregistered group runner.
- - Unregistered project runner.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/335509) in GitLab 14.8, audit events for when a runner is registered.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/349540) in GitLab 14.9, audit events for when a runner is unregistered.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/349542) in GitLab 14.9, audit events for when a runner is assigned to or unassigned from a project.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/355637) in GitLab 15.0, audit events for when a runner registration token is reset.
+
+GitLab generates audit events for the following GitLab Runner actions:
+
+- Instance, group, or project runner is registered.
+- Instance, group, or project runner is unregistered.
+- Runner is assigned to or unassigned from a project.
+- Instance, group, or project runner registration token is reset.
+ [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102579) in GitLab 15.6.
## "Deleted User" events
diff --git a/doc/api/keys.md b/doc/api/keys.md
index 90144310238..337758e6c33 100644
--- a/doc/api/keys.md
+++ b/doc/api/keys.md
@@ -11,15 +11,15 @@ If using a SHA256 fingerprint in an API call, you should URL-encode the fingerpr
## Get SSH key with user by ID of an SSH key
-Get SSH key with user by ID of an SSH key. Note only administrators can lookup SSH key with user by ID of an SSH key.
+Get SSH key with user by ID of an SSH key. Only available to administrators.
```plaintext
GET /keys/:id
```
-| Attribute | Type | Required | Description |
-|:----------|:--------|:---------|:---------------------|
-| `id` | integer | yes | The ID of an SSH key |
+| Attribute | Type | Required | Description |
+|:----------|:--------|:---------|:----------------------|
+| `id` | integer | yes | The ID of an SSH key. |
Example request:
@@ -78,9 +78,9 @@ You can search for a user that owns a specific SSH key. Note only administrators
GET /keys
```
-| Attribute | Type | Required | Description |
-|:--------------|:-------|:---------|:------------------------------|
-| `fingerprint` | string | yes | The fingerprint of an SSH key |
+| Attribute | Type | Required | Description |
+|:--------------|:-------|:---------|:-------------------------------|
+| `fingerprint` | string | yes | The fingerprint of an SSH key. |
Example request:
diff --git a/doc/api/project_import_export.md b/doc/api/project_import_export.md
index f09e4d2785a..9728332f8f8 100644
--- a/doc/api/project_import_export.md
+++ b/doc/api/project_import_export.md
@@ -46,14 +46,15 @@ POST /projects/:id/export
| Attribute | Type | Required | Description |
| --------- | -------------- | -------- | ---------------------------------------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user |
-| `description` | string | no | Overrides the project description |
-| `upload` | hash | no | Hash that contains the information to upload the exported project to a web server |
-| `upload[url]` | string | yes | The URL to upload the project |
-| `upload[http_method]` | string | no | The HTTP method to upload the exported project. Only `PUT` and `POST` methods allowed. Default is `PUT` |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user.
+| `upload[url]` | string | yes | The URL to upload the project.
+| `description` | string | no | Overrides the project description.
+| `upload` | hash | no | Hash that contains the information to upload the exported project to a web server.
+| `upload[http_method]` | string | no | The HTTP method to upload the exported project. Only `PUT` and `POST` methods allowed. Default is `PUT`.
```shell
-curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/export" \
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
+ "https://gitlab.example.com/api/v4/projects/1/export" \
--data "upload[http_method]=PUT" \
--data-urlencode "upload[url]=https://example-bucket.s3.eu-west-3.amazonaws.com/backup?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIMBJHN2O62W8IELQ%2F20180312%2Feu-west-3%2Fs3%2Faws4_request&X-Amz-Date=20180312T110328Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=8413facb20ff33a49a147a0b4abcff4c8487cc33ee1f7e450c46e8f695569dbd"
```
@@ -74,10 +75,11 @@ GET /projects/:id/export
| Attribute | Type | Required | Description |
| --------- | -------------- | -------- | ---------------------------------------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user.
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/export"
+curl --header "PRIVATE-TOKEN: <your_access_token>" \
+ "https://gitlab.example.com/api/v4/projects/1/export"
```
Status can be one of:
@@ -120,9 +122,9 @@ Download the finished export.
GET /projects/:id/export/download
```
-| Attribute | Type | Required | Description |
-| --------- | -------------- | -------- | ---------------------------------------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user |
+| Attribute | Type | Required | Description |
+| --------- | ----------------- | -------- | ---------------------------------------- |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user.
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" --remote-header-name \
@@ -140,14 +142,14 @@ ls *export.tar.gz
POST /projects/import
```
-| Attribute | Type | Required | Description |
-| --------- | -------------- | -------- | ---------------------------------------- |
-| `namespace` | integer/string | no | The ID or path of the namespace to import the project to. Defaults to the current user's namespace.<br/><br/> Requires at least the Maintainer role on the destination group to import to. Using the Developer role for this purpose was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/387891) in GitLab 15.8 and will be removed in GitLab 16.0. |
-| `name` | string | no | The name of the project to be imported. Defaults to the path of the project if not provided |
-| `file` | string | yes | The file to be uploaded |
-| `path` | string | yes | Name and path for new project |
-| `overwrite` | boolean | no | If there is a project with the same path the import overwrites it. Default to false |
-| `override_params` | Hash | no | Supports all fields defined in the [Project API](projects.md) |
+| Attribute | Type | Required | Description |
+| ----------- | -------------- | -------- | ---------------------------------------- |
+| `file` | string | yes | The file to be uploaded.
+| `path` | string | yes | Name and path for new project.
+| `name` | string | no | The name of the project to be imported. Defaults to the path of the project if not provided.
+| `namespace` | integer or string | no | The ID or path of the namespace to import the project to. Defaults to the current user's namespace.<br/><br/> Requires at least the Maintainer role on the destination group to import to. Using the Developer role for this purpose was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/387891) in GitLab 15.8 and is scheduled for removal in GitLab 16.0.
+| `override_params` | Hash | no | Supports all fields defined in the [Project API](projects.md).
+| `overwrite` | boolean | no | If there is a project with the same path the import overwrites it. Defaults to `false`.
The override parameters passed take precedence over all values defined inside the export file.
@@ -196,39 +198,29 @@ requests.post(url, headers=headers, data=data, files=files)
```
NOTE:
-The maximum import file size can be set by the Administrator, default is `0` (unlimited)..
+The maximum import file size can be set by the Administrator. It defaults to `0` (unlimited).
As an administrator, you can modify the maximum import file size. To do so, use the `max_import_size` option in the [Application settings API](settings.md#change-application-settings) or the [Admin Area](../user/admin_area/settings/account_and_limit_settings.md). Default [modified](https://gitlab.com/gitlab-org/gitlab/-/issues/251106) from 50 MB to 0 in GitLab 13.8.
-## Import a file from a remote object storage
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/282503) in GitLab 13.12 in [Beta](../policy/alpha-beta-support.md#beta-features).
-
-This endpoint is behind a feature flag that is enabled by default.
+## Import a file from a remote object storage (Beta)
-To enable this endpoint:
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/282503) in GitLab 13.12 in [Beta](../policy/alpha-beta-support.md#beta-features) [with a flag](../administration/feature_flags.md) named `import_project_from_remote_file`. Enabled by default.
-```ruby
-Feature.enable(:import_project_from_remote_file)
-```
-
-To disable this endpoint:
-
-```ruby
-Feature.disable(:import_project_from_remote_file)
-```
+FLAG:
+On self-managed GitLab, by default this feature is available. To hide the feature, ask an administrator to [disable the feature flag](../administration/feature_flags.md) named `import_project_from_remote_file`.
+On GitLab.com, this feature is available.
```plaintext
POST /projects/remote-import
```
-| Attribute | Type | Required | Description |
-| ----------------- | -------------- | -------- | ---------------------------------------- |
-| `namespace` | integer/string | no | The ID or path of the namespace to import the project to. Defaults to the current user's namespace. |
-| `name` | string | no | The name of the project to import. If not provided, defaults to the path of the project. |
-| `url` | string | yes | URL for the file to import. |
-| `path` | string | yes | Name and path for the new project. |
-| `overwrite` | boolean | no | Whether to overwrite a project with the same path when importing. Defaults to `false`. |
-| `override_params` | Hash | no | Supports all fields defined in the [Project API](projects.md). |
+| Attribute | Type | Required | Description |
+| ----------------- | ----------------- | -------- | ---------------------------------------- |
+| `path` | string | yes | Name and path for the new project.
+| `url` | string | yes | URL for the file to import.
+| `name` | string | no | The name of the project to import. If not provided, defaults to the path of the project.
+| `namespace` | integer or string | no | The ID or path of the namespace to import the project to. Defaults to the current user's namespace.
+| `overwrite` | boolean | no | Whether to overwrite a project with the same path when importing. Defaults to `false`.
+| `override_params` | Hash | no | Supports all fields defined in the [Project API](projects.md).
The passed override parameters take precedence over all values defined in the export file.
@@ -256,7 +248,7 @@ curl --request POST \
}
```
-The `Content-Length` header must return a valid number. The maximum file size is 10 gigabytes.
+The `Content-Length` header must return a valid number. The maximum file size is 10 GB.
The `Content-Type` header must be `application/gzip`.
## Import a file from AWS S3
@@ -273,14 +265,14 @@ POST /projects/remote-import-s3
| Attribute | Type | Required | Description |
| ------------------- | -------------- | -------- | ---------------------------------------- |
-| `namespace` | integer/string | no | The ID or path of the namespace to import the project to. Defaults to the current user's namespace. |
-| `name` | string | no | The name of the project to import. If not provided, defaults to the path of the project. |
-| `path` | string | yes | The full path of the new project. |
-| `region` | string | yes | [AWS S3 region name where the file is stored.](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html#Regions) |
-| `bucket_name` | string | yes | [AWS S3 bucket name where the file is stored.](https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html) |
-| `file_key` | string | yes | [AWS S3 file key to identify the file.](https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingObjects.html) |
-| `access_key_id` | string | yes | [AWS S3 access key ID.](https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html#access-keys-and-secret-access-keys). |
-| `secret_access_key` | string | yes | [AWS S3 secret access key.](https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html#access-keys-and-secret-access-keys) |
+| `access_key_id` | string | yes | [AWS S3 access key ID](https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html#access-keys-and-secret-access-keys).
+| `bucket_name` | string | yes | [AWS S3 bucket name](https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html) where the file is stored.
+| `file_key` | string | yes | [AWS S3 file key](https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingObjects.html) to identify the file.
+| `path` | string | yes | The full path of the new project.
+| `region` | string | yes | [AWS S3 region name](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html#Regions) where the file is stored.
+| `secret_access_key` | string | yes | [AWS S3 secret access key](https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html#access-keys-and-secret-access-keys).
+| `name` | string | no | The name of the project to import. If not provided, defaults to the path of the project.
+| `namespace` | integer or string | no | The ID or path of the namespace to import the project to. Defaults to the current user's namespace.
The passed override parameters take precedence over all values defined in the export file.
@@ -347,10 +339,11 @@ GET /projects/:id/import
| Attribute | Type | Required | Description |
| --------- | -------------- | -------- | ---------------------------------------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user.
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/import"
+curl --header "PRIVATE-TOKEN: <your_access_token>" \
+ "https://gitlab.example.com/api/v4/projects/1/import"
```
Status can be one of:
@@ -363,8 +356,10 @@ Status can be one of:
If the status is `failed`, it includes the import error message under `import_error`.
If the status is `failed`, `started` or `finished`, the `failed_relations` array might
-be populated with any occurrences of relations that failed to import either due to
-unrecoverable errors or because retries were exhausted (a typical example are query timeouts.)
+be populated with any occurrences of relations that failed to import due to either:
+
+- Unrecoverable errors.
+- Retries were exhausted. A typical example: query timeouts.
NOTE:
An element's `id` field in `failed_relations` references the failure record, not the relation.
diff --git a/doc/api/repositories.md b/doc/api/repositories.md
index d56a855b745..d3bee8de2c5 100644
--- a/doc/api/repositories.md
+++ b/doc/api/repositories.md
@@ -164,7 +164,8 @@ Supported attributes:
Example request:
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.com/api/v4/projects/<project_id>/repository/archive?sha=<commit_sha>&path=<path>"
+curl --header "PRIVATE-TOKEN: <your_access_token>" \
+ "https://gitlab.com/api/v4/projects/<project_id>/repository/archive?sha=<commit_sha>&path=<path>"
```
## Compare branches, tags or commits
@@ -278,10 +279,11 @@ GET /projects/:id/repository/merge_base
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
| `refs` | array | yes | The refs to find the common ancestor of. Accepts multiple refs. |
-Example request:
+Example request, with the refs truncated for readability:
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/repository/merge_base?refs[]=304d257dcb821665ab5110318fc58a007bd104ed&refs[]=0031876facac3f2b2702a0e53a26e89939a42209"
+curl --header "PRIVATE-TOKEN: <your_access_token>" \
+ "https://gitlab.example.com/api/v4/projects/5/repository/merge_base?refs[]=304d257d&refs[]=0031876f"
```
Example response:
@@ -385,26 +387,30 @@ If the last tag is `v0.9.0` and the default branch is `main`, the range of commi
included in this example is `v0.9.0..main`:
```shell
-curl --request POST --header "PRIVATE-TOKEN: token" --data "version=1.0.0" "https://gitlab.com/api/v4/projects/42/repository/changelog"
+curl --request POST --header "PRIVATE-TOKEN: token" \
+ --data "version=1.0.0" "https://gitlab.com/api/v4/projects/42/repository/changelog"
```
To generate the data on a different branch, specify the `branch` parameter. This
command generates data from the `foo` branch:
```shell
-curl --request POST --header "PRIVATE-TOKEN: token" --data "version=1.0.0&branch=foo" "https://gitlab.com/api/v4/projects/42/repository/changelog"
+curl --request POST --header "PRIVATE-TOKEN: token" \
+ --data "version=1.0.0&branch=foo" "https://gitlab.com/api/v4/projects/42/repository/changelog"
```
To use a different trailer, use the `trailer` parameter:
```shell
-curl --request POST --header "PRIVATE-TOKEN: token" --data "version=1.0.0&trailer=Type" "https://gitlab.com/api/v4/projects/42/repository/changelog"
+curl --request POST --header "PRIVATE-TOKEN: token" \
+ --data "version=1.0.0&trailer=Type" "https://gitlab.com/api/v4/projects/42/repository/changelog"
```
To store the results in a different file, use the `file` parameter:
```shell
-curl --request POST --header "PRIVATE-TOKEN: token" --data "version=1.0.0&file=NEWS" "https://gitlab.com/api/v4/projects/42/repository/changelog"
+curl --request POST --header "PRIVATE-TOKEN: token" \
+ --data "version=1.0.0&file=NEWS" "https://gitlab.com/api/v4/projects/42/repository/changelog"
```
## Generate changelog data
@@ -426,21 +432,26 @@ Supported attributes:
| Attribute | Type | Required | Description |
| :-------- | :------- | :--------- | :---------- |
| `version` | string | yes | The version to generate the changelog for. The format must follow [semantic versioning](https://semver.org/). |
-| `config_file` | string | no | The path of changelog configuration file in the project's Git repository, defaults to `.gitlab/changelog_config.yml`. |
-| `date` | datetime | no | The date and time of the release, ISO 8601 formatted. Example: `2016-03-11T03:45:40Z`. Defaults to the current time. |
+| `config_file` | string | no | The path of changelog configuration file in the project's Git repository. Defaults to `.gitlab/changelog_config.yml`. |
+| `date` | datetime | no | The date and time of the release. Uses ISO 8601 format. Example: `2016-03-11T03:45:40Z`. Defaults to the current time. |
| `from` | string | no | The start of the range of commits (as a SHA) to use for generating the changelog. This commit itself isn't included in the list. |
| `to` | string | no | The end of the range of commits (as a SHA) to use for the changelog. This commit _is_ included in the list. Defaults to the HEAD of the default project branch. |
-| `trailer` | string | no | The Git trailer to use for including commits, defaults to `Changelog`. |
+| `trailer` | string | no | The Git trailer to use for including commits. Defaults to `Changelog`. |
```shell
-curl --header "PRIVATE-TOKEN: token" "https://gitlab.com/api/v4/projects/42/repository/changelog?version=1.0.0"
+curl --header "PRIVATE-TOKEN: token" \
+ "https://gitlab.com/api/v4/projects/42/repository/changelog?version=1.0.0"
```
-Example Response:
+Example response, with line breaks added for readability:
```json
{
- "notes": "## 1.0.0 (2021-11-17)\n\n### feature (2 changes)\n\n- [Title 2](namespace13/project13@ad608eb642124f5b3944ac0ac772fecaf570a6bf) ([merge request](namespace13/project13!2))\n- [Title 1](namespace13/project13@3c6b80ff7034fa0d585314e1571cc780596ce3c8) ([merge request](namespace13/project13!1))\n"
+ "notes": "## 1.0.0 (2021-11-17)\n\n### feature (2 changes)\n\n-
+ [Title 2](namespace13/project13@ad608eb642124f5b3944ac0ac772fecaf570a6bf)
+ ([merge request](namespace13/project13!2))\n-
+ [Title 1](namespace13/project13@3c6b80ff7034fa0d585314e1571cc780596ce3c8)
+ ([merge request](namespace13/project13!1))\n"
}
```
diff --git a/doc/ci/environments/external_deployment_tools.md b/doc/ci/environments/external_deployment_tools.md
index 58d2705b490..37c4cf05f13 100644
--- a/doc/ci/environments/external_deployment_tools.md
+++ b/doc/ci/environments/external_deployment_tools.md
@@ -14,7 +14,7 @@ GitLab can receive deployment events from these external tools and allows you to
For example, the following features are available by setting up tracking:
- [See when an merge request has been deployed, and to which environment](../../user/project/merge_requests/widgets.md#post-merge-pipeline-status).
-- [Filter merge requests by environment or deployment date](../../user/project/merge_requests/index.md#filter-merge-requests-by-environment-or-deployment-date).
+- [Filter merge requests by environment or deployment date](../../user/project/merge_requests/index.md#by-environment-or-deployment-date).
- [DevOps Research and Assessment (DORA) metrics](../../user/analytics/dora_metrics.md).
- [View environments and deployments](index.md#view-environments-and-deployments).
- [Track newly included merge requests per deployment](index.md#track-newly-included-merge-requests-per-deployment).
diff --git a/doc/development/graphql_guide/reviewing.md b/doc/development/graphql_guide/reviewing.md
index eee2e285957..c98158a43be 100644
--- a/doc/development/graphql_guide/reviewing.md
+++ b/doc/development/graphql_guide/reviewing.md
@@ -37,7 +37,7 @@ For more information, see [deprecation and removal process](../../api/graphql/in
Ensure that multi-version compatibility is guaranteed.
This generally means frontend and backend code for the same GraphQL feature can't be shipped in the same release.
-For details, see [multiple version compatibility](../multi_version_compatibility.md).[multiple version compatibility](../multi_version_compatibility.md).
+For details, see [multiple version compatibility](../multi_version_compatibility.md).
### Technical writing review
diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md
index 345330827b9..e5e83384e3a 100644
--- a/doc/user/group/saml_sso/index.md
+++ b/doc/user/group/saml_sso/index.md
@@ -469,11 +469,11 @@ than 24 hours ago, GitLab prompts the user to sign in again through SSO.
SSO is enforced as follows:
| Project/Group visibility | Enforce SSO setting | Member with identity | Member without identity | Non-member or not signed in |
-|--------------------------|---------------------|--------------------| ------ |------------------------------|
-| Private | Off | Enforced | Not enforced | No access |
-| Private | On | Enforced | Enforced | No access |
-| Public | Off | Enforced | Not enforced | Not enforced |
-| Public | On | Enforced | Enforced | Not enforced |
+|--------------------------|---------------------|----------------------|-------------------------|-----------------------------|
+| Private | Off | Enforced | Not enforced | Not enforced |
+| Private | On | Enforced | Enforced | Enforced |
+| Public | Off | Enforced | Not enforced | Not enforced |
+| Public | On | Enforced | Enforced | Not enforced |
An [issue exists](https://gitlab.com/gitlab-org/gitlab/-/issues/297389) to add a similar SSO requirement for API activity.
@@ -481,7 +481,7 @@ An [issue exists](https://gitlab.com/gitlab-org/gitlab/-/issues/297389) to add a
When the **Enforce SSO-only authentication for web activity for this group** option is enabled:
-- All users must access GitLab by using their GitLab group's single sign-on URL
+- All members must access GitLab by using their GitLab group's single sign-on URL
to access group resources, regardless of whether they have an existing SAML
identity.
- SSO is enforced when users access groups and projects in the organization's
@@ -489,6 +489,9 @@ When the **Enforce SSO-only authentication for web activity for this group** opt
- Users cannot be added as new members manually.
- Users with the Owner role can use the standard sign in process to make
necessary changes to top-level group settings.
+- For non-members or users who are not signed in:
+ - SSO is not enforced when they access public group resources.
+ - SSO is enforced when they access private group resources.
SSO enforcement for web activity has the following effects when enabled:
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 0a526e1d268..ee7f4e5dfed 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -30,7 +30,7 @@ Learn the various ways to [create a merge request](creating_merge_requests.md).
You can view merge requests for your project, group, or yourself.
-### View merge requests for a project
+### For a project
To view all merge requests for a project:
@@ -39,7 +39,7 @@ To view all merge requests for a project:
Or, to use a [keyboard shortcut](../../shortcuts.md), press <kbd>g</kbd> + <kbd>m</kbd>.
-### View merge requests for all projects in a group
+### For all projects in a group
To view merge requests for all projects in a group:
@@ -48,7 +48,7 @@ To view merge requests for all projects in a group:
If your group contains subgroups, this view also displays merge requests from the subgroup projects.
-### View all merge requests assigned to you
+### Assigned to you
To view all merge requests assigned to you:
@@ -79,7 +79,7 @@ To filter the list of merge requests:
1. Above the list of merge requests, select **Search or filter results...**.
1. From the dropdown list, select the attribute you wish to filter by. Some examples:
- - [**By environment or deployment date**](#filter-merge-requests-by-environment-or-deployment-date).
+ - [**By environment or deployment date**](#by-environment-or-deployment-date).
- **ID**: Enter filter `#30` to return only merge request 30.
- User filters: Type (or select from the dropdown list) any of these filters to display a list of users:
- **Approved-By**, for merge requests already approved by a user. **(PREMIUM)**.
@@ -100,7 +100,7 @@ To filter the list of merge requests:
GitLab displays the results on-screen, but you can also
[retrieve them as an RSS feed](../../search/index.md#retrieve-search-results-as-feed).
-### Filter merge requests by environment or deployment date
+### By environment or deployment date
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44041) in GitLab 13.6.
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 8505e029f3a..d0c4d7f101a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -18519,6 +18519,9 @@ msgstr ""
msgid "ForksDivergence|Create a merge request to your project's default branch."
msgstr ""
+msgid "ForksDivergence|Create merge request"
+msgstr ""
+
msgid "ForksDivergence|Failed to fetch fork details. Try again later."
msgstr ""
@@ -21042,12 +21045,21 @@ msgstr ""
msgid "Groups|Checking group URL availability..."
msgstr ""
+msgid "Groups|Create and add README"
+msgstr ""
+
+msgid "Groups|Creating README"
+msgstr ""
+
msgid "Groups|Enter a descriptive name for your group."
msgstr ""
msgid "Groups|Group ID"
msgstr ""
+msgid "Groups|Group README"
+msgstr ""
+
msgid "Groups|Group URL"
msgstr ""
@@ -21093,6 +21105,15 @@ msgstr ""
msgid "Groups|Subgroup slug"
msgstr ""
+msgid "Groups|There was an error creating the Group README."
+msgstr ""
+
+msgid "Groups|This will create a README.md for project %{path}."
+msgstr ""
+
+msgid "Groups|This will create a project %{path} and add a README.md."
+msgstr ""
+
msgid "Groups|You're creating a new top-level group"
msgstr ""
diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb
index 88f78b87d08..1a49b0497ba 100644
--- a/spec/features/groups/group_settings_spec.rb
+++ b/spec/features/groups/group_settings_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Edit group settings', feature_category: :subgroups do
+ include Spec::Support::Helpers::ModalHelpers
+
let(:user) { create(:user) }
let(:group) { create(:group, path: 'foo') }
@@ -244,6 +246,77 @@ RSpec.describe 'Edit group settings', feature_category: :subgroups do
end
end
+ describe 'group README', :js do
+ let_it_be(:group) { create(:group) }
+
+ context 'with gitlab-profile project and README.md' do
+ let_it_be(:project) { create(:project, :readme, namespace: group) }
+
+ it 'renders link to Group README and navigates to it on click' do
+ visit edit_group_path(group)
+ wait_for_requests
+
+ click_link('README')
+ wait_for_requests
+
+ expect(page).to have_current_path(project_blob_path(project, "#{project.default_branch}/README.md"))
+ expect(page).to have_text('README.md')
+ end
+ end
+
+ context 'with gitlab-profile project and no README.md' do
+ let_it_be(:project) { create(:project, name: 'gitlab-profile', namespace: group) }
+
+ it 'renders Add README button and allows user to create a README via the IDE' do
+ visit edit_group_path(group)
+ wait_for_requests
+
+ expect(page).not_to have_selector('.ide')
+
+ click_button('Add README')
+
+ accept_gl_confirm("This will create a README.md for project #{group.readme_project.present.path_with_namespace}.", button_text: 'Add README')
+ wait_for_requests
+
+ expect(page).to have_current_path("/-/ide/project/#{group.readme_project.present.path_with_namespace}/edit/main/-/README.md/")
+
+ page.within('.ide') do
+ expect(page).to have_text('README.md')
+ end
+ end
+ end
+
+ context 'with no gitlab-profile project and no README.md' do
+ it 'renders Add README button and allows user to create both the gitlab-profile project and README via the IDE' do
+ visit edit_group_path(group)
+ wait_for_requests
+
+ expect(page).not_to have_selector('.ide')
+
+ click_button('Add README')
+
+ accept_gl_confirm("This will create a project #{group.full_path}/gitlab-profile and add a README.md.", button_text: 'Create and add README')
+ wait_for_requests
+
+ expect(page).to have_current_path("/-/ide/project/#{group.full_path}/gitlab-profile/edit/main/-/README.md/")
+
+ page.within('.ide') do
+ expect(page).to have_text('README.md')
+ end
+ end
+ end
+
+ describe 'with :show_group_readme FF false' do
+ before do
+ stub_feature_flags(show_group_readme: false)
+ end
+
+ it 'does not render Group README settings' do
+ expect(page).not_to have_text('README')
+ end
+ end
+ end
+
def update_path(new_group_path)
visit edit_group_path(group)
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index d1f8929ee8a..2ae347d4f9e 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -86,7 +86,7 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
end
within '.js-right-sidebar' do
- find('.block.assignee').click(x: 0, y: 0)
+ find('.block.assignee').click(x: 0, y: 0, offset: 0)
find('.block.assignee .edit-link').click
end
diff --git a/spec/finders/groups/accepting_project_creations_finder_spec.rb b/spec/finders/groups/accepting_project_creations_finder_spec.rb
new file mode 100644
index 00000000000..b1b9403748d
--- /dev/null
+++ b/spec/finders/groups/accepting_project_creations_finder_spec.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::AcceptingProjectCreationsFinder, feature_category: :subgroups do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group_where_direct_owner) { create(:group) }
+ let_it_be(:subgroup_of_group_where_direct_owner) { create(:group, parent: group_where_direct_owner) }
+ let_it_be(:group_where_direct_maintainer) { create(:group) }
+ let_it_be(:group_where_direct_maintainer_but_cant_create_projects) do
+ create(:group, project_creation_level: Gitlab::Access::NO_ONE_PROJECT_ACCESS)
+ end
+
+ let_it_be(:group_where_direct_developer_but_developers_cannot_create_projects) { create(:group) }
+ let_it_be(:group_where_direct_developer) do
+ create(:group, project_creation_level: Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS)
+ end
+
+ let_it_be(:shared_with_group_where_direct_owner_as_owner) { create(:group) }
+
+ let_it_be(:shared_with_group_where_direct_owner_as_developer) do
+ create(:group, project_creation_level: Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS)
+ end
+
+ let_it_be(:shared_with_group_where_direct_owner_as_developer_but_developers_cannot_create_projects) do
+ create(:group)
+ end
+
+ let_it_be(:shared_with_group_where_direct_developer_as_maintainer) do
+ create(:group, project_creation_level: Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS)
+ end
+
+ let_it_be(:shared_with_group_where_direct_owner_as_guest) { create(:group) }
+ let_it_be(:shared_with_group_where_direct_owner_as_maintainer) { create(:group) }
+ let_it_be(:shared_with_group_where_direct_developer_as_owner) do
+ create(:group, project_creation_level: Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS)
+ end
+
+ let_it_be(:subgroup_of_shared_with_group_where_direct_owner_as_maintainer) do
+ create(:group, parent: shared_with_group_where_direct_owner_as_maintainer)
+ end
+
+ before do
+ group_where_direct_owner.add_owner(user)
+ group_where_direct_maintainer.add_maintainer(user)
+ group_where_direct_developer_but_developers_cannot_create_projects.add_developer(user)
+ group_where_direct_developer.add_developer(user)
+
+ create(:group_group_link, :owner,
+ shared_with_group: group_where_direct_owner,
+ shared_group: shared_with_group_where_direct_owner_as_owner
+ )
+
+ create(:group_group_link, :developer,
+ shared_with_group: group_where_direct_owner,
+ shared_group: shared_with_group_where_direct_owner_as_developer_but_developers_cannot_create_projects
+ )
+
+ create(:group_group_link, :maintainer,
+ shared_with_group: group_where_direct_developer,
+ shared_group: shared_with_group_where_direct_developer_as_maintainer
+ )
+
+ create(:group_group_link, :developer,
+ shared_with_group: group_where_direct_owner,
+ shared_group: shared_with_group_where_direct_owner_as_developer
+ )
+
+ create(:group_group_link, :guest,
+ shared_with_group: group_where_direct_owner,
+ shared_group: shared_with_group_where_direct_owner_as_guest
+ )
+
+ create(:group_group_link, :maintainer,
+ shared_with_group: group_where_direct_owner,
+ shared_group: shared_with_group_where_direct_owner_as_maintainer
+ )
+
+ create(:group_group_link, :owner,
+ shared_with_group: group_where_direct_developer_but_developers_cannot_create_projects,
+ shared_group: shared_with_group_where_direct_developer_as_owner
+ )
+ end
+
+ describe '#execute' do
+ subject(:result) { described_class.new(user).execute }
+
+ it 'only returns groups where the user has access to create projects' do
+ expect(result).to match_array([
+ group_where_direct_owner,
+ subgroup_of_group_where_direct_owner,
+ group_where_direct_maintainer,
+ group_where_direct_developer,
+ # groups arising from group shares
+ shared_with_group_where_direct_owner_as_owner,
+ shared_with_group_where_direct_owner_as_maintainer,
+ subgroup_of_shared_with_group_where_direct_owner_as_maintainer,
+ shared_with_group_where_direct_developer_as_owner,
+ shared_with_group_where_direct_developer_as_maintainer,
+ shared_with_group_where_direct_owner_as_developer
+ ])
+ end
+
+ context 'when `include_groups_from_group_shares_in_project_creation_locations` flag is disabled' do
+ before do
+ stub_feature_flags(include_groups_from_group_shares_in_project_creation_locations: false)
+ end
+
+ it 'returns only groups accessible via direct membership where user has access to create projects' do
+ expect(result).to match_array([
+ group_where_direct_owner,
+ subgroup_of_group_where_direct_owner,
+ group_where_direct_maintainer,
+ group_where_direct_developer
+ ])
+ end
+ end
+ end
+end
diff --git a/spec/frontend/api/projects_api_spec.js b/spec/frontend/api/projects_api_spec.js
index 2d54d6173fd..4ceed885e6e 100644
--- a/spec/frontend/api/projects_api_spec.js
+++ b/spec/frontend/api/projects_api_spec.js
@@ -67,6 +67,20 @@ describe('~/api/projects_api.js', () => {
});
});
+ describe('createProject', () => {
+ it('posts to the correct URL and returns the data', () => {
+ const body = { name: 'test project' };
+ const expectedUrl = '/api/v7/projects.json';
+ const expectedRes = { id: 999, name: 'test project' };
+
+ mock.onPost(expectedUrl, body).replyOnce(HTTP_STATUS_OK, { data: expectedRes });
+
+ return projectsApi.createProject(body).then(({ data }) => {
+ expect(data).toStrictEqual(expectedRes);
+ });
+ });
+ });
+
describe('importProjectMembers', () => {
beforeEach(() => {
jest.spyOn(axios, 'post');
diff --git a/spec/frontend/groups/settings/components/group_settings_readme_spec.js b/spec/frontend/groups/settings/components/group_settings_readme_spec.js
new file mode 100644
index 00000000000..8d4da73934f
--- /dev/null
+++ b/spec/frontend/groups/settings/components/group_settings_readme_spec.js
@@ -0,0 +1,112 @@
+import { GlModal, GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import GroupSettingsReadme from '~/groups/settings/components/group_settings_readme.vue';
+import { GITLAB_README_PROJECT } from '~/groups/settings/constants';
+import {
+ MOCK_GROUP_PATH,
+ MOCK_GROUP_ID,
+ MOCK_PATH_TO_GROUP_README,
+ MOCK_PATH_TO_README_PROJECT,
+} from '../mock_data';
+
+describe('GroupSettingsReadme', () => {
+ let wrapper;
+
+ const defaultProps = {
+ groupPath: MOCK_GROUP_PATH,
+ groupId: MOCK_GROUP_ID,
+ };
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(GroupSettingsReadme, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ stubs: {
+ GlModal,
+ GlSprintf,
+ },
+ });
+ };
+
+ const findHasReadmeButtonLink = () => wrapper.findByText('README');
+ const findAddReadmeButton = () => wrapper.findByTestId('group-settings-add-readme-button');
+ const findModalBody = () => wrapper.findByTestId('group-settings-modal-readme-body');
+ const findModalCreateReadmeButton = () =>
+ wrapper.findByTestId('group-settings-modal-create-readme-button');
+
+ describe('Group has existing README', () => {
+ beforeEach(() => {
+ createComponent({
+ groupReadmePath: MOCK_PATH_TO_GROUP_README,
+ readmeProjectPath: MOCK_PATH_TO_README_PROJECT,
+ });
+ });
+
+ describe('template', () => {
+ it('renders README Button Link with correct path and text', () => {
+ expect(findHasReadmeButtonLink().exists()).toBe(true);
+ expect(findHasReadmeButtonLink().attributes('href')).toBe(MOCK_PATH_TO_GROUP_README);
+ });
+
+ it('does not render Add README Button', () => {
+ expect(findAddReadmeButton().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('Group has README project without README file', () => {
+ beforeEach(() => {
+ createComponent({ readmeProjectPath: MOCK_PATH_TO_README_PROJECT });
+ });
+
+ describe('template', () => {
+ it('does not render README', () => {
+ expect(findHasReadmeButtonLink().exists()).toBe(false);
+ });
+
+ it('does render Add Readme Button with correct text', () => {
+ expect(findAddReadmeButton().exists()).toBe(true);
+ expect(findAddReadmeButton().text()).toBe('Add README');
+ });
+
+ it('generates a hidden modal with correct body text', () => {
+ expect(findModalBody().text()).toMatchInterpolatedText(
+ `This will create a README.md for project ${MOCK_PATH_TO_README_PROJECT}.`,
+ );
+ });
+
+ it('generates a hidden modal with correct button text', () => {
+ expect(findModalCreateReadmeButton().text()).toBe('Add README');
+ });
+ });
+ });
+
+ describe('Group does not have README project', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('template', () => {
+ it('does not render README', () => {
+ expect(findHasReadmeButtonLink().exists()).toBe(false);
+ });
+
+ it('does render Add Readme Button with correct text', () => {
+ expect(findAddReadmeButton().exists()).toBe(true);
+ expect(findAddReadmeButton().text()).toBe('Add README');
+ });
+
+ it('generates a hidden modal with correct body text', () => {
+ expect(findModalBody().text()).toMatchInterpolatedText(
+ `This will create a project ${MOCK_GROUP_PATH}/${GITLAB_README_PROJECT} and add a README.md.`,
+ );
+ });
+
+ it('generates a hidden modal with correct button text', () => {
+ expect(findModalCreateReadmeButton().text()).toBe('Create and add README');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/groups/settings/mock_data.js b/spec/frontend/groups/settings/mock_data.js
new file mode 100644
index 00000000000..4551ee3318b
--- /dev/null
+++ b/spec/frontend/groups/settings/mock_data.js
@@ -0,0 +1,6 @@
+export const MOCK_GROUP_PATH = 'test-group';
+export const MOCK_GROUP_ID = '999';
+
+export const MOCK_PATH_TO_GROUP_README = '/group/project/-/blob/main/README.md';
+
+export const MOCK_PATH_TO_README_PROJECT = 'group/project';
diff --git a/spec/frontend/lib/utils/web_ide_navigator_spec.js b/spec/frontend/lib/utils/web_ide_navigator_spec.js
new file mode 100644
index 00000000000..0f5cd09d50e
--- /dev/null
+++ b/spec/frontend/lib/utils/web_ide_navigator_spec.js
@@ -0,0 +1,38 @@
+import { visitUrl, webIDEUrl } from '~/lib/utils/url_utility';
+import { openWebIDE } from '~/lib/utils/web_ide_navigator';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ visitUrl: jest.fn(),
+ webIDEUrl: jest.fn().mockImplementation((path) => `/-/ide/projects${path}`),
+}));
+
+describe('openWebIDE', () => {
+ it('when called without projectPath throws TypeError and does not call visitUrl', () => {
+ expect(() => {
+ openWebIDE();
+ }).toThrow(new TypeError('projectPath parameter is required'));
+ expect(visitUrl).not.toHaveBeenCalled();
+ });
+
+ it('when called with projectPath and without fileName calls visitUrl with correct path', () => {
+ const params = { projectPath: 'project-path' };
+ const expectedNonIDEPath = `/${params.projectPath}/edit/main/-/`;
+ const expectedIDEPath = `/-/ide/projects${expectedNonIDEPath}`;
+
+ openWebIDE(params.projectPath);
+
+ expect(webIDEUrl).toHaveBeenCalledWith(expectedNonIDEPath);
+ expect(visitUrl).toHaveBeenCalledWith(expectedIDEPath);
+ });
+
+ it('when called with projectPath and fileName calls visitUrl with correct path', () => {
+ const params = { projectPath: 'project-path', fileName: 'README' };
+ const expectedNonIDEPath = `/${params.projectPath}/edit/main/-/${params.fileName}/`;
+ const expectedIDEPath = `/-/ide/projects${expectedNonIDEPath}`;
+
+ openWebIDE(params.projectPath, params.fileName);
+
+ expect(webIDEUrl).toHaveBeenCalledWith(expectedNonIDEPath);
+ expect(visitUrl).toHaveBeenCalledWith(expectedIDEPath);
+ });
+});
diff --git a/spec/frontend/repository/components/fork_info_spec.js b/spec/frontend/repository/components/fork_info_spec.js
index 4fadedf6b83..75310ba8ca4 100644
--- a/spec/frontend/repository/components/fork_info_spec.js
+++ b/spec/frontend/repository/components/fork_info_spec.js
@@ -84,7 +84,8 @@ describe('ForkInfo component', () => {
const findLink = () => wrapper.findComponent(GlLink);
const findSkeleton = () => wrapper.findComponent(GlSkeletonLoader);
const findIcon = () => wrapper.findComponent(GlIcon);
- const findUpdateForkButton = () => wrapper.findComponent(GlButton);
+ const findUpdateForkButton = () => wrapper.findByTestId('update-fork-button');
+ const findCreateMrButton = () => wrapper.findByTestId('create-mr-button');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findDivergenceMessage = () => wrapper.findByTestId('divergence-message');
const findInaccessibleMessage = () => wrapper.findByTestId('inaccessible-project');
@@ -139,6 +140,16 @@ describe('ForkInfo component', () => {
expect(link.attributes('href')).toBe(propsForkInfo.sourcePath);
});
+ it('renders Create MR Button with correct path', async () => {
+ await createComponent();
+ expect(findCreateMrButton().attributes('href')).toBe(propsForkInfo.createMrPath);
+ });
+
+ it('does not render create MR button if user had no permission to Create MR in fork', async () => {
+ await createComponent({ canUserCreateMrInFork: false });
+ expect(findCreateMrButton().exists()).toBe(false);
+ });
+
it('renders alert with error message when request fails', async () => {
mockForkDetailsQuery.mockRejectedValue(forkInfoError);
await createComponent({});
@@ -170,7 +181,7 @@ describe('ForkInfo component', () => {
});
await createComponent({});
expect(findUpdateForkButton().exists()).toBe(true);
- expect(findUpdateForkButton().text()).toBe(i18n.sync);
+ expect(findUpdateForkButton().text()).toBe(i18n.updateFork);
});
});
@@ -211,7 +222,8 @@ describe('ForkInfo component', () => {
message: '3 commits behind, 7 commits ahead of the upstream repository.',
firstLink: propsForkInfo.behindComparePath,
secondLink: propsForkInfo.aheadComparePath,
- hasButton: true,
+ hasUpdateButton: true,
+ hasCreateMrButton: true,
},
{
ahead: 7,
@@ -219,7 +231,8 @@ describe('ForkInfo component', () => {
message: '7 commits ahead of the upstream repository.',
firstLink: propsForkInfo.aheadComparePath,
secondLink: '',
- hasButton: false,
+ hasUpdateButton: false,
+ hasCreateMrButton: true,
},
{
ahead: 0,
@@ -227,11 +240,12 @@ describe('ForkInfo component', () => {
message: '3 commits behind the upstream repository.',
firstLink: propsForkInfo.behindComparePath,
secondLink: '',
- hasButton: true,
+ hasUpdateButton: true,
+ hasCreateMrButton: false,
},
])(
'renders correct divergence message for ahead: $ahead, behind: $behind divergence commits',
- ({ ahead, behind, message, firstLink, secondLink, hasButton }) => {
+ ({ ahead, behind, message, firstLink, secondLink, hasUpdateButton, hasCreateMrButton }) => {
beforeEach(async () => {
mockResolvedForkDetailsQuery({ ahead, behind, isSyncing: false, hasConflicts: false });
await createComponent({});
@@ -251,9 +265,16 @@ describe('ForkInfo component', () => {
});
it('renders Update Fork button when fork is behind', () => {
- expect(findUpdateForkButton().exists()).toBe(hasButton);
- if (hasButton) {
- expect(findUpdateForkButton().text()).toBe(i18n.sync);
+ expect(findUpdateForkButton().exists()).toBe(hasUpdateButton);
+ if (hasUpdateButton) {
+ expect(findUpdateForkButton().text()).toBe(i18n.updateFork);
+ }
+ });
+
+ it('renders Create Merge Request button when fork is ahead', () => {
+ expect(findCreateMrButton().exists()).toBe(hasCreateMrButton);
+ if (hasCreateMrButton) {
+ expect(findCreateMrButton().text()).toBe(i18n.createMergeRequest);
}
});
},
diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js
index 418a93a10cc..c7cc2820588 100644
--- a/spec/frontend/repository/mock_data.js
+++ b/spec/frontend/repository/mock_data.js
@@ -125,6 +125,8 @@ export const propsForkInfo = {
sourcePath: 'gitlab-org/gitlab',
aheadComparePath: '/nataliia/myGitLab/-/compare/main...ref?from_project_id=1',
behindComparePath: 'gitlab-org/gitlab/-/compare/ref...main?from_project_id=2',
+ createMrPath: 'path/to/new/mr',
+ canUserCreateMrInFork: true,
};
export const propsConflictsModal = {
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index ff9662f672b..9d28147bd74 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -1363,11 +1363,13 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
source_project = project_with_repo
allow(helper).to receive(:visible_fork_source).with(project).and_return(source_project)
+ allow(helper).to receive(:can_user_create_mr_in_fork).with(source_project).and_return(false)
ahead_path =
"/#{project.full_path}/-/compare/#{source_project.default_branch}...ref?from_project_id=#{source_project.id}"
behind_path =
"/#{source_project.full_path}/-/compare/ref...#{source_project.default_branch}?from_project_id=#{project.id}"
+ create_mr_path = "/#{project.full_path}/-/merge_requests/new?merge_request%5Bsource_branch%5D=ref&merge_request%5Btarget_branch%5D=#{source_project.default_branch}&merge_request%5Btarget_project_id%5D=#{source_project.id}"
expect(helper.vue_fork_divergence_data(project, 'ref')).to eq({
project_path: project.full_path,
@@ -1376,7 +1378,9 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
source_path: project_path(source_project),
ahead_compare_path: ahead_path,
behind_compare_path: behind_path,
- source_default_branch: source_project.default_branch
+ source_default_branch: source_project.default_branch,
+ create_mr_path: create_mr_path,
+ can_user_create_mr_in_fork: false
})
end
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 715749a4195..4a300cc45b5 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -216,7 +216,7 @@ merge_requests:
- approver_groups
- approved_by_users
- draft_notes
-- merge_train
+- merge_train_car
- blocks_as_blocker
- blocks_as_blockee
- blocking_merge_requests
@@ -873,6 +873,7 @@ incident_management_setting:
- project
merge_trains:
- project
+merge_train_cars:
- merge_request
boards:
- group
diff --git a/spec/models/concerns/expirable_spec.rb b/spec/models/concerns/expirable_spec.rb
index 50dfb138ac9..68a25917ce1 100644
--- a/spec/models/concerns/expirable_spec.rb
+++ b/spec/models/concerns/expirable_spec.rb
@@ -3,40 +3,52 @@
require 'spec_helper'
RSpec.describe Expirable do
- describe 'ProjectMember' do
- let_it_be(:no_expire) { create(:project_member) }
- let_it_be(:expire_later) { create(:project_member, expires_at: 8.days.from_now) }
- let_it_be(:expired) { create(:project_member, expires_at: 1.day.from_now) }
+ let_it_be(:no_expire) { create(:project_member) }
+ let_it_be(:expire_later) { create(:project_member, expires_at: 8.days.from_now) }
+ let_it_be(:expired) { create(:project_member, expires_at: 1.day.from_now) }
- before do
- travel_to(3.days.from_now)
- end
+ before do
+ travel_to(3.days.from_now)
+ end
- describe '.expired' do
- it { expect(ProjectMember.expired).to match_array([expired]) }
- end
+ describe '.expired' do
+ it { expect(ProjectMember.expired).to match_array([expired]) }
- describe '.not_expired' do
- it { expect(ProjectMember.not_expired).to include(no_expire, expire_later) }
- it { expect(ProjectMember.not_expired).not_to include(expired) }
- end
+ it 'scopes the query when multiple models are expirable' do
+ expired_access_token = create(:personal_access_token, :expired, user: no_expire.user)
- describe '#expired?' do
- it { expect(no_expire.expired?).to eq(false) }
- it { expect(expire_later.expired?).to eq(false) }
- it { expect(expired.expired?).to eq(true) }
+ expect(PersonalAccessToken.expired.joins(user: :members)).to match_array([expired_access_token])
+ expect(PersonalAccessToken.joins(user: :members).merge(ProjectMember.expired)).to eq([])
end
- describe '#expires?' do
- it { expect(no_expire.expires?).to eq(false) }
- it { expect(expire_later.expires?).to eq(true) }
- it { expect(expired.expires?).to eq(true) }
- end
+ it 'works with a timestamp expired_at field', time_travel_to: '2022-03-14T11:30:00Z' do
+ expired_deploy_token = create(:deploy_token, expires_at: 5.minutes.ago.iso8601)
- describe '#expires_soon?' do
- it { expect(no_expire.expires_soon?).to eq(false) }
- it { expect(expire_later.expires_soon?).to eq(true) }
- it { expect(expired.expires_soon?).to eq(true) }
+ # Here verify that `expires_at` in the SQL uses `Time.current` instead of `Date.current`
+ expect(DeployToken.expired).to match_array([expired_deploy_token])
end
end
+
+ describe '.not_expired' do
+ it { expect(ProjectMember.not_expired).to include(no_expire, expire_later) }
+ it { expect(ProjectMember.not_expired).not_to include(expired) }
+ end
+
+ describe '#expired?' do
+ it { expect(no_expire.expired?).to eq(false) }
+ it { expect(expire_later.expired?).to eq(false) }
+ it { expect(expired.expired?).to eq(true) }
+ end
+
+ describe '#expires?' do
+ it { expect(no_expire.expires?).to eq(false) }
+ it { expect(expire_later.expires?).to eq(true) }
+ it { expect(expired.expires?).to eq(true) }
+ end
+
+ describe '#expires_soon?' do
+ it { expect(no_expire.expires_soon?).to eq(false) }
+ it { expect(expire_later.expires_soon?).to eq(true) }
+ it { expect(expired.expires_soon?).to eq(true) }
+ end
end
diff --git a/spec/models/group_group_link_spec.rb b/spec/models/group_group_link_spec.rb
index eec8fe0ef71..59370cf12d2 100644
--- a/spec/models/group_group_link_spec.rb
+++ b/spec/models/group_group_link_spec.rb
@@ -5,9 +5,29 @@ require 'spec_helper'
RSpec.describe GroupGroupLink do
let_it_be(:group) { create(:group) }
let_it_be(:shared_group) { create(:group) }
- let_it_be(:group_group_link) do
- create(:group_group_link, shared_group: shared_group,
- shared_with_group: group)
+
+ describe 'validation' do
+ let_it_be(:group_group_link) do
+ create(:group_group_link, shared_group: shared_group,
+ shared_with_group: group)
+ end
+
+ it { is_expected.to validate_presence_of(:shared_group) }
+
+ it do
+ is_expected.to(
+ validate_uniqueness_of(:shared_group_id)
+ .scoped_to(:shared_with_group_id)
+ .with_message('The group has already been shared with this group'))
+ end
+
+ it { is_expected.to validate_presence_of(:shared_with_group) }
+ it { is_expected.to validate_presence_of(:group_access) }
+
+ it do
+ is_expected.to(
+ validate_inclusion_of(:group_access).in_array(Gitlab::Access.values))
+ end
end
describe 'relations' do
@@ -16,42 +36,51 @@ RSpec.describe GroupGroupLink do
end
describe 'scopes' do
- describe '.non_guests' do
- let!(:group_group_link_reporter) { create :group_group_link, :reporter }
- let!(:group_group_link_maintainer) { create :group_group_link, :maintainer }
- let!(:group_group_link_owner) { create :group_group_link, :owner }
- let!(:group_group_link_guest) { create :group_group_link, :guest }
-
- it 'returns all records which are greater than Guests access' do
- expect(described_class.non_guests).to match_array([
- group_group_link_reporter, group_group_link,
- group_group_link_maintainer, group_group_link_owner
- ])
- end
- end
-
- describe '.with_owner_or_maintainer_access' do
+ context 'for scopes fetching records based on access levels' do
+ let_it_be(:group_group_link_guest) { create :group_group_link, :guest }
+ let_it_be(:group_group_link_reporter) { create :group_group_link, :reporter }
+ let_it_be(:group_group_link_developer) { create :group_group_link, :developer }
let_it_be(:group_group_link_maintainer) { create :group_group_link, :maintainer }
let_it_be(:group_group_link_owner) { create :group_group_link, :owner }
- let_it_be(:group_group_link_reporter) { create :group_group_link, :reporter }
- let_it_be(:group_group_link_guest) { create :group_group_link, :guest }
- it 'returns all records which have OWNER or MAINTAINER access' do
- expect(described_class.with_owner_or_maintainer_access).to match_array([
- group_group_link_maintainer,
- group_group_link_owner
- ])
+ describe '.non_guests' do
+ it 'returns all records which are greater than Guests access' do
+ expect(described_class.non_guests).to match_array([
+ group_group_link_reporter, group_group_link_developer,
+ group_group_link_maintainer, group_group_link_owner
+ ])
+ end
end
- end
- describe '.with_owner_access' do
- let_it_be(:group_group_link_maintainer) { create :group_group_link, :maintainer }
- let_it_be(:group_group_link_owner) { create :group_group_link, :owner }
- let_it_be(:group_group_link_reporter) { create :group_group_link, :reporter }
- let_it_be(:group_group_link_guest) { create :group_group_link, :guest }
+ describe '.with_owner_or_maintainer_access' do
+ it 'returns all records which have OWNER or MAINTAINER access' do
+ expect(described_class.with_owner_or_maintainer_access).to match_array([
+ group_group_link_maintainer,
+ group_group_link_owner
+ ])
+ end
+ end
- it 'returns all records which have OWNER access' do
- expect(described_class.with_owner_access).to match_array([group_group_link_owner])
+ describe '.with_owner_access' do
+ it 'returns all records which have OWNER access' do
+ expect(described_class.with_owner_access).to match_array([group_group_link_owner])
+ end
+ end
+
+ describe '.with_developer_access' do
+ it 'returns all records which have DEVELOPER access' do
+ expect(described_class.with_developer_access).to match_array([group_group_link_developer])
+ end
+ end
+
+ describe '.with_developer_maintainer_owner_access' do
+ it 'returns all records which have DEVELOPER, MAINTAINER or OWNER access' do
+ expect(described_class.with_developer_maintainer_owner_access).to match_array([
+ group_group_link_developer,
+ group_group_link_owner,
+ group_group_link_maintainer
+ ])
+ end
end
end
@@ -93,6 +122,15 @@ RSpec.describe GroupGroupLink do
let_it_be(:sub_shared_group) { create(:group, parent: shared_group) }
let_it_be(:other_group) { create(:group) }
+ let_it_be(:group_group_link_1) do
+ create(
+ :group_group_link,
+ shared_group: shared_group,
+ shared_with_group: group,
+ group_access: Gitlab::Access::DEVELOPER
+ )
+ end
+
let_it_be(:group_group_link_2) do
create(
:group_group_link,
@@ -125,7 +163,7 @@ RSpec.describe GroupGroupLink do
expect(described_class.all.count).to eq(4)
expect(distinct_group_group_links.count).to eq(2)
- expect(distinct_group_group_links).to include(group_group_link)
+ expect(distinct_group_group_links).to include(group_group_link_1)
expect(distinct_group_group_links).not_to include(group_group_link_2)
expect(distinct_group_group_links).not_to include(group_group_link_3)
expect(distinct_group_group_links).to include(group_group_link_4)
@@ -133,27 +171,9 @@ RSpec.describe GroupGroupLink do
end
end
- describe 'validation' do
- it { is_expected.to validate_presence_of(:shared_group) }
-
- it do
- is_expected.to(
- validate_uniqueness_of(:shared_group_id)
- .scoped_to(:shared_with_group_id)
- .with_message('The group has already been shared with this group'))
- end
-
- it { is_expected.to validate_presence_of(:shared_with_group) }
- it { is_expected.to validate_presence_of(:group_access) }
-
- it do
- is_expected.to(
- validate_inclusion_of(:group_access).in_array(Gitlab::Access.values))
- end
- end
-
describe '#human_access' do
it 'delegates to Gitlab::Access' do
+ group_group_link = create(:group_group_link, :reporter)
expect(Gitlab::Access).to receive(:human_access).with(group_group_link.group_access)
group_group_link.human_access
@@ -161,6 +181,8 @@ RSpec.describe GroupGroupLink do
end
describe 'search by group name' do
+ let_it_be(:group_group_link) { create(:group_group_link, :reporter, shared_with_group: group) }
+
it { expect(described_class.search(group.name)).to eq([group_group_link]) }
it { expect(described_class.search('not-a-group-name')).to be_empty }
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 8fd9cb0be28..0bf4540f535 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -969,6 +969,23 @@ RSpec.describe Group, feature_category: :subgroups do
end
end
+ describe '.with_project_creation_levels' do
+ let_it_be(:group_1) { create(:group, project_creation_level: Gitlab::Access::NO_ONE_PROJECT_ACCESS) }
+ let_it_be(:group_2) { create(:group, project_creation_level: Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) }
+ let_it_be(:group_3) { create(:group, project_creation_level: Gitlab::Access::MAINTAINER_PROJECT_ACCESS) }
+ let_it_be(:group_4) { create(:group, project_creation_level: nil) }
+
+ it 'returns groups with the specified project creation levels' do
+ result = described_class.with_project_creation_levels([
+ Gitlab::Access::NO_ONE_PROJECT_ACCESS,
+ Gitlab::Access::MAINTAINER_PROJECT_ACCESS
+ ])
+
+ expect(result).to include(group_1, group_3)
+ expect(result).not_to include(group_2, group_4)
+ end
+ end
+
describe '.project_creation_allowed' do
let_it_be(:group_1) { create(:group, project_creation_level: Gitlab::Access::NO_ONE_PROJECT_ACCESS) }
let_it_be(:group_2) { create(:group, project_creation_level: Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) }
diff --git a/spec/views/groups/settings/_general.html.haml_spec.rb b/spec/views/groups/settings/_general.html.haml_spec.rb
new file mode 100644
index 00000000000..d58e25c0d99
--- /dev/null
+++ b/spec/views/groups/settings/_general.html.haml_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'groups/settings/_general.html.haml', feature_category: :subgroups do
+ describe 'Group Settings README' do
+ let_it_be(:group) { build_stubbed(:group) }
+ let_it_be(:user) { build_stubbed(:admin) }
+
+ before do
+ assign(:group, group)
+ allow(view).to receive(:current_user).and_return(user)
+ end
+
+ describe 'with :show_group_readme FF true' do
+ before do
+ stub_feature_flags(show_group_readme: true)
+ end
+
+ it 'renders #js-group-settings-readme' do
+ render
+
+ expect(rendered).to have_selector('#js-group-settings-readme')
+ end
+ end
+
+ describe 'with :show_group_readme FF false' do
+ before do
+ stub_feature_flags(show_group_readme: false)
+ end
+
+ it 'does not render #js-group-settings-readme' do
+ render
+
+ expect(rendered).not_to have_selector('#js-group-settings-readme')
+ end
+ end
+ end
+end