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