Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-04-11 15:08:32 +00:00
parent 28e90894e1
commit f1ce71c88c
49 changed files with 1147 additions and 239 deletions

View File

@ -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'

View File

@ -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"},

View File

@ -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)

View File

@ -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)

View File

@ -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>

View File

@ -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';

View File

@ -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,
},
});
},
});
};

View File

@ -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('/')}/`));
};

View File

@ -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();

View File

@ -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,
},
});
},

View File

@ -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"

View File

@ -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);

View File

@ -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>

View File

@ -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"

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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, ""

View File

@ -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?

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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();

View File

@ -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

View File

@ -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:

View File

@ -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.

View File

@ -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"
}
```

View File

@ -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).

View File

@ -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

View File

@ -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:

View File

@ -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.

View File

@ -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 ""

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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');

View File

@ -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');
});
});
});
});

View File

@ -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';

View File

@ -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);
});
});

View File

@ -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);
}
});
},

View File

@ -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 = {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -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