Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									28e90894e1
								
							
						
					
					
						commit
						f1ce71c88c
					
				
							
								
								
									
										2
									
								
								Gemfile
								
								
								
								
							
							
						
						
									
										2
									
								
								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' | ||||
| 
 | ||||
|  |  | |||
|  | @ -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"}, | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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> | ||||
|  | @ -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'; | ||||
|  |  | |||
|  | @ -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, | ||||
|         }, | ||||
|       }); | ||||
|     }, | ||||
|   }); | ||||
| }; | ||||
|  | @ -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('/')}/`)); | ||||
| }; | ||||
|  | @ -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(); | ||||
|  |  | |||
|  | @ -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, | ||||
|         }, | ||||
|       }); | ||||
|     }, | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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, "" | ||||
|  |  | |||
|  | @ -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? | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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(); | ||||
|  |  | |||
|  | @ -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, 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. | ||||
| 
 | ||||
| - [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. | ||||
| 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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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: | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
| ## Import a file from a remote object storage (Beta) | ||||
| 
 | ||||
| > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/282503) in GitLab 13.12 in [Beta](../policy/alpha-beta-support.md#beta-features). | ||||
| > [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. | ||||
| 
 | ||||
| This endpoint is behind a feature flag that is enabled by default. | ||||
| 
 | ||||
| To enable this endpoint: | ||||
| 
 | ||||
| ```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. | ||||
|  |  | |||
|  | @ -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" | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
|  |  | |||
|  | @ -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). | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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: | ||||
| 
 | ||||
|  |  | |||
|  | @ -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. | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 "" | ||||
| 
 | ||||
|  |  | |||
|  | @ -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) | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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'); | ||||
|  |  | |||
|  | @ -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'); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | @ -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'; | ||||
|  | @ -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); | ||||
|   }); | ||||
| }); | ||||
|  | @ -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); | ||||
|         } | ||||
|       }); | ||||
|     }, | ||||
|  |  | |||
|  | @ -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 = { | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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) | ||||
|   before do | ||||
|     travel_to(3.days.from_now) | ||||
|   end | ||||
| 
 | ||||
|   describe '.expired' do | ||||
|     it { expect(ProjectMember.expired).to match_array([expired]) } | ||||
| 
 | ||||
|     it 'scopes the query when multiple models are expirable' do | ||||
|       expired_access_token = create(:personal_access_token, :expired, user: no_expire.user) | ||||
| 
 | ||||
|       expect(PersonalAccessToken.expired.joins(user: :members)).to match_array([expired_access_token]) | ||||
|       expect(PersonalAccessToken.joins(user: :members).merge(ProjectMember.expired)).to eq([]) | ||||
|     end | ||||
| 
 | ||||
|     describe '.expired' do | ||||
|       it { expect(ProjectMember.expired).to match_array([expired]) } | ||||
|     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 '.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) } | ||||
|       # 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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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) } | ||||
|  |  | |||
|  | @ -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 | ||||
		Loading…
	
		Reference in New Issue