Commit 1c2c9fd9 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 75ef20e2 964390f4
import { Node } from '@tiptap/core';
import { PARSE_HTML_PRIORITY_LOWEST } from '../constants';
export default Node.create({
name: 'division',
content: 'block*',
group: 'block',
defining: true,
parseHTML() {
return [{ tag: 'div', priority: PARSE_HTML_PRIORITY_LOWEST }];
},
renderHTML({ HTMLAttributes }) {
return ['div', HTMLAttributes, 0];
},
});
import { Node } from '@tiptap/core';
export default Node.create({
name: 'figure',
content: 'block+',
group: 'block',
defining: true,
parseHTML() {
return [{ tag: 'figure' }];
},
renderHTML({ HTMLAttributes }) {
return ['figure', HTMLAttributes, 0];
},
});
import { Node } from '@tiptap/core';
export default Node.create({
name: 'figureCaption',
content: 'inline*',
group: 'block',
defining: true,
parseHTML() {
return [{ tag: 'figcaption' }];
},
renderHTML({ HTMLAttributes }) {
return ['figcaption', HTMLAttributes, 0];
},
});
...@@ -8,9 +8,12 @@ import Bold from '../extensions/bold'; ...@@ -8,9 +8,12 @@ import Bold from '../extensions/bold';
import BulletList from '../extensions/bullet_list'; import BulletList from '../extensions/bullet_list';
import Code from '../extensions/code'; import Code from '../extensions/code';
import CodeBlockHighlight from '../extensions/code_block_highlight'; import CodeBlockHighlight from '../extensions/code_block_highlight';
import Division from '../extensions/division';
import Document from '../extensions/document'; import Document from '../extensions/document';
import Dropcursor from '../extensions/dropcursor'; import Dropcursor from '../extensions/dropcursor';
import Emoji from '../extensions/emoji'; import Emoji from '../extensions/emoji';
import Figure from '../extensions/figure';
import FigureCaption from '../extensions/figure_caption';
import Gapcursor from '../extensions/gapcursor'; import Gapcursor from '../extensions/gapcursor';
import HardBreak from '../extensions/hard_break'; import HardBreak from '../extensions/hard_break';
import Heading from '../extensions/heading'; import Heading from '../extensions/heading';
...@@ -71,8 +74,11 @@ export const createContentEditor = ({ ...@@ -71,8 +74,11 @@ export const createContentEditor = ({
Code, Code,
CodeBlockHighlight, CodeBlockHighlight,
Document, Document,
Division,
Dropcursor, Dropcursor,
Emoji, Emoji,
Figure,
FigureCaption,
Gapcursor, Gapcursor,
HardBreak, HardBreak,
Heading, Heading,
......
...@@ -9,7 +9,10 @@ import Bold from '../extensions/bold'; ...@@ -9,7 +9,10 @@ import Bold from '../extensions/bold';
import BulletList from '../extensions/bullet_list'; import BulletList from '../extensions/bullet_list';
import Code from '../extensions/code'; import Code from '../extensions/code';
import CodeBlockHighlight from '../extensions/code_block_highlight'; import CodeBlockHighlight from '../extensions/code_block_highlight';
import Division from '../extensions/division';
import Emoji from '../extensions/emoji'; import Emoji from '../extensions/emoji';
import Figure from '../extensions/figure';
import FigureCaption from '../extensions/figure_caption';
import HardBreak from '../extensions/hard_break'; import HardBreak from '../extensions/hard_break';
import Heading from '../extensions/heading'; import Heading from '../extensions/heading';
import HorizontalRule from '../extensions/horizontal_rule'; import HorizontalRule from '../extensions/horizontal_rule';
...@@ -43,6 +46,7 @@ import { ...@@ -43,6 +46,7 @@ import {
renderOrderedList, renderOrderedList,
renderImage, renderImage,
renderPlayable, renderPlayable,
renderHTMLNode,
} from './serialization_helpers'; } from './serialization_helpers';
const defaultSerializerConfig = { const defaultSerializerConfig = {
...@@ -116,11 +120,14 @@ const defaultSerializerConfig = { ...@@ -116,11 +120,14 @@ const defaultSerializerConfig = {
state.write('```'); state.write('```');
state.closeBlock(node); state.closeBlock(node);
}, },
[Division.name]: renderHTMLNode('div'),
[Emoji.name]: (state, node) => { [Emoji.name]: (state, node) => {
const { name } = node.attrs; const { name } = node.attrs;
state.write(`:${name}:`); state.write(`:${name}:`);
}, },
[Figure.name]: renderHTMLNode('figure'),
[FigureCaption.name]: renderHTMLNode('figcaption'),
[HardBreak.name]: renderHardBreak, [HardBreak.name]: renderHardBreak,
[Heading.name]: defaultMarkdownSerializer.nodes.heading, [Heading.name]: defaultMarkdownSerializer.nodes.heading,
[HorizontalRule.name]: defaultMarkdownSerializer.nodes.horizontal_rule, [HorizontalRule.name]: defaultMarkdownSerializer.nodes.horizontal_rule,
......
...@@ -24,12 +24,20 @@ export function isPlainURL(link, parent, index, side) { ...@@ -24,12 +24,20 @@ export function isPlainURL(link, parent, index, side) {
return !link.isInSet(next.marks); return !link.isInSet(next.marks);
} }
function shouldRenderCellInline(cell) { function containsOnlyText(node) {
if (node.childCount === 1) {
const child = node.child(0);
return child.isText && child.marks.length === 0;
}
return false;
}
function containsParagraphWithOnlyText(cell) {
if (cell.childCount === 1) { if (cell.childCount === 1) {
const parent = cell.child(0); const child = cell.child(0);
if (parent.type.name === 'paragraph' && parent.childCount === 1) { if (child.type.name === 'paragraph') {
const child = parent.child(0); return containsOnlyText(child);
return child.isText && child.marks.length === 0;
} }
} }
...@@ -208,7 +216,7 @@ function renderTableRowAsHTML(state, node) { ...@@ -208,7 +216,7 @@ function renderTableRowAsHTML(state, node) {
renderTagOpen(state, tag, cell.attrs); renderTagOpen(state, tag, cell.attrs);
if (!shouldRenderCellInline(cell)) { if (!containsParagraphWithOnlyText(cell)) {
state.closeBlock(node); state.closeBlock(node);
state.flushClose(); state.flushClose();
} }
...@@ -222,6 +230,38 @@ function renderTableRowAsHTML(state, node) { ...@@ -222,6 +230,38 @@ function renderTableRowAsHTML(state, node) {
renderTagClose(state, 'tr'); renderTagClose(state, 'tr');
} }
export function renderContent(state, node, forceRenderInline) {
if (node.type.inlineContent) {
if (containsOnlyText(node)) {
state.renderInline(node);
} else {
state.closeBlock(node);
state.flushClose();
state.renderInline(node);
state.closeBlock(node);
state.flushClose();
}
} else {
const renderInline = forceRenderInline || containsParagraphWithOnlyText(node);
if (!renderInline) {
state.closeBlock(node);
state.flushClose();
state.renderContent(node);
state.ensureNewLine();
} else {
state.renderInline(forceRenderInline ? node : node.child(0));
}
}
}
export function renderHTMLNode(tagName, forceRenderInline = false) {
return (state, node) => {
renderTagOpen(state, tagName, node.attrs);
renderContent(state, node, forceRenderInline);
renderTagClose(state, tagName, false);
};
}
export function renderOrderedList(state, node) { export function renderOrderedList(state, node) {
const { parens } = node.attrs; const { parens } = node.attrs;
const start = node.attrs.start || 1; const start = node.attrs.start || 1;
...@@ -241,7 +281,7 @@ export function renderTableCell(state, node) { ...@@ -241,7 +281,7 @@ export function renderTableCell(state, node) {
return; return;
} }
if (!isInBlockTable(node) || shouldRenderCellInline(node)) { if (!isInBlockTable(node) || containsParagraphWithOnlyText(node)) {
state.renderInline(node.child(0)); state.renderInline(node.child(0));
} else { } else {
state.renderContent(node); state.renderContent(node);
......
...@@ -1550,7 +1550,11 @@ class User < ApplicationRecord ...@@ -1550,7 +1550,11 @@ class User < ApplicationRecord
end end
def manageable_groups(include_groups_with_developer_maintainer_access: false) def manageable_groups(include_groups_with_developer_maintainer_access: false)
owned_and_maintainer_group_hierarchy = Gitlab::ObjectHierarchy.new(owned_or_maintainers_groups).base_and_descendants owned_and_maintainer_group_hierarchy = if Feature.enabled?(:linear_user_manageable_groups, self, default_enabled: :yaml)
owned_or_maintainers_groups.self_and_descendants
else
Gitlab::ObjectHierarchy.new(owned_or_maintainers_groups).base_and_descendants
end
if include_groups_with_developer_maintainer_access if include_groups_with_developer_maintainer_access
union_sql = ::Gitlab::SQL::Union.new( union_sql = ::Gitlab::SQL::Union.new(
......
...@@ -12,6 +12,7 @@ module Projects ...@@ -12,6 +12,7 @@ module Projects
@import_data = @params.delete(:import_data) @import_data = @params.delete(:import_data)
@relations_block = @params.delete(:relations_block) @relations_block = @params.delete(:relations_block)
@default_branch = @params.delete(:default_branch) @default_branch = @params.delete(:default_branch)
@readme_template = @params.delete(:readme_template)
build_topics build_topics
end end
...@@ -149,12 +150,16 @@ module Projects ...@@ -149,12 +150,16 @@ module Projects
branch_name: @default_branch.presence || @project.default_branch_or_main, branch_name: @default_branch.presence || @project.default_branch_or_main,
commit_message: 'Initial commit', commit_message: 'Initial commit',
file_path: 'README.md', file_path: 'README.md',
file_content: experiment(:new_project_readme_content, namespace: @project.namespace).run_with(@project) file_content: readme_content
} }
Files::CreateService.new(@project, current_user, commit_attrs).execute Files::CreateService.new(@project, current_user, commit_attrs).execute
end end
def readme_content
@readme_template.presence || experiment(:new_project_readme_content, namespace: @project.namespace).run_with(@project)
end
def skip_wiki? def skip_wiki?
!@project.feature_available?(:wiki, current_user) || @skip_wiki !@project.feature_available?(:wiki, current_user) || @skip_wiki
end end
......
---
name: linear_user_manageable_groups
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68845
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339434
milestone: '14.3'
type: development
group: group::access
default_enabled: false
...@@ -40,7 +40,10 @@ export default { ...@@ -40,7 +40,10 @@ export default {
return this.isEditing ? this.policyType : this.newPolicyType; return this.isEditing ? this.policyType : this.newPolicyType;
}, },
isEditing() { isEditing() {
return Boolean(this.existingPolicy?.creation_timestamp || this.existingPolicy?.updatedAt); return Boolean(
this.existingPolicy?.creation_timestamp ||
this.existingPolicy?.type === POLICY_TYPE_COMPONENT_OPTIONS.scanExecution.urlParameter,
);
}, },
policyTypes() { policyTypes() {
return Object.values(POLICY_TYPE_COMPONENT_OPTIONS); return Object.values(POLICY_TYPE_COMPONENT_OPTIONS);
......
...@@ -4,6 +4,7 @@ module Security ...@@ -4,6 +4,7 @@ module Security
module SecurityOrchestrationPolicies module SecurityOrchestrationPolicies
class ProjectCreateService < ::BaseProjectService class ProjectCreateService < ::BaseProjectService
ACCESS_LEVELS_TO_ADD = [Gitlab::Access::MAINTAINER, Gitlab::Access::DEVELOPER].freeze ACCESS_LEVELS_TO_ADD = [Gitlab::Access::MAINTAINER, Gitlab::Access::DEVELOPER].freeze
README_TEMPLATE_PATH = Rails.root.join('ee', 'app', 'views', 'projects', 'security', 'policies', 'readme.md.tt')
def execute def execute
return error('Security Policy project already exists.') if project.security_orchestration_policy_configuration.present? return error('Security Policy project already exists.') if project.security_orchestration_policy_configuration.present?
...@@ -41,10 +42,15 @@ module Security ...@@ -41,10 +42,15 @@ module Security
requirements_enabled: false, requirements_enabled: false,
builds_enabled: false, builds_enabled: false,
wiki_enabled: false, wiki_enabled: false,
snippets_enabled: false snippets_enabled: false,
readme_template: readme_template
} }
end end
def readme_template
ERB.new(File.read(README_TEMPLATE_PATH), trim_mode: '<>').result(binding)
end
attr_reader :project attr_reader :project
end end
end end
......
# Security Policy Project for <%= @project.name %>
This project is automatically generated to manage security policies for the project.
The Security Policies Project is a repository used to store policies. All security policies are stored as a YAML file named `.gitlab/security-policies/policy.yml`, with this format:
```yaml
---
scan_execution_policy:
- name: Enforce DAST in every pipeline
description: This policy enforces pipeline configuration to have a job with DAST scan
enabled: true
rules:
- type: pipeline
branches:
- master
actions:
- scan: dast
scanner_profile: Scanner Profile A
site_profile: Site Profile B
- name: Enforce DAST in every pipeline in the main branch
description: This policy enforces pipeline configuration to have a job with DAST scan for the main branch
enabled: true
rules:
- type: pipeline
branches:
- main
actions:
- scan: dast
scanner_profile: Scanner Profile C
site_profile: Site Profile D
```
You can read more about the format and policies schema in the [documentation](https://docs.gitlab.com/ee/user/application_security/policies/#scan-execution-policies-schema).
## Default branch protection settings
This project is preconfigured with the default branch set as a protected branch, and only [project](<%= @project.web_url %>)
maintainers/owners have permission to merge into that branch. This overrides any default branch protection both at the
[group level](https://docs.gitlab.com/ee/user/group/index.html#change-the-default-branch-protection-of-a-group) and at the
[instance level](https://docs.gitlab.com/ee/user/admin_area/settings/visibility_and_access_controls.html#default-branch-protection).
...@@ -93,9 +93,9 @@ describe('PolicyEditor component', () => { ...@@ -93,9 +93,9 @@ describe('PolicyEditor component', () => {
describe('when an existing policy is present', () => { describe('when an existing policy is present', () => {
it.each` it.each`
policyType | option | existingPolicy | findComponent policyType | option | existingPolicy | findComponent
${'container_policy'} | ${POLICY_TYPE_COMPONENT_OPTIONS.container} | ${{ manifest: mockL3Manifest, updatedAt: '2020-04-14T00:08:30Z' }} | ${findNeworkPolicyEditor} ${'container_policy'} | ${POLICY_TYPE_COMPONENT_OPTIONS.container} | ${{ manifest: mockL3Manifest, creation_timestamp: '2020-04-14T00:08:30Z' }} | ${findNeworkPolicyEditor}
${'scan_execution_policy'} | ${POLICY_TYPE_COMPONENT_OPTIONS.scanExecution} | ${mockDastScanExecutionObject} | ${findScanExecutionPolicyEditor} ${'scan_execution_policy'} | ${POLICY_TYPE_COMPONENT_OPTIONS.scanExecution} | ${mockDastScanExecutionObject} | ${findScanExecutionPolicyEditor}
`( `(
'renders the disabled form select for existing policy of type $policyType', 'renders the disabled form select for existing policy of type $policyType',
async ({ existingPolicy, findComponent, option, policyType }) => { async ({ existingPolicy, findComponent, option, policyType }) => {
......
...@@ -6,6 +6,6 @@ import { ...@@ -6,6 +6,6 @@ import {
describe('fromYaml', () => { describe('fromYaml', () => {
it('returns policy object', () => { it('returns policy object', () => {
expect(fromYaml(mockDastScanExecutionManifest)).toMatchObject(mockDastScanExecutionObject); expect(fromYaml(mockDastScanExecutionManifest)).toStrictEqual(mockDastScanExecutionObject);
}); });
}); });
...@@ -57,7 +57,6 @@ rules: ...@@ -57,7 +57,6 @@ rules:
- type: pipeline - type: pipeline
branches: branches:
- main - main
updatedAt: '2020-04-14T00:08:30Z'
actions: actions:
- scan: dast - scan: dast
site_profile: required_site_profile site_profile: required_site_profile
...@@ -70,7 +69,6 @@ export const mockDastScanExecutionObject = { ...@@ -70,7 +69,6 @@ export const mockDastScanExecutionObject = {
description: 'This policy enforces pipeline configuration to have a job with DAST scan', description: 'This policy enforces pipeline configuration to have a job with DAST scan',
enabled: false, enabled: false,
rules: [{ type: 'pipeline', branches: ['main'] }], rules: [{ type: 'pipeline', branches: ['main'] }],
updatedAt: '2020-04-14T00:08:30Z',
actions: [ actions: [
{ {
scan: 'dast', scan: 'dast',
......
...@@ -18,7 +18,7 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProjectCreateService do ...@@ -18,7 +18,7 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProjectCreateService do
project.add_developer(developer) project.add_developer(developer)
end end
it 'creates policy project with maintainers and developers from target project as developers' do it 'creates policy project with maintainers and developers from target project as developers', :aggregate_failures do
response = service.execute response = service.execute
policy_project = response[:policy_project] policy_project = response[:policy_project]
...@@ -26,6 +26,8 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProjectCreateService do ...@@ -26,6 +26,8 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProjectCreateService do
expect(policy_project.namespace).to eq(project.namespace) expect(policy_project.namespace).to eq(project.namespace)
expect(policy_project.team.developers).to contain_exactly(maintainer, developer) expect(policy_project.team.developers).to contain_exactly(maintainer, developer)
expect(policy_project.container_registry_access_level).to eq(ProjectFeature::DISABLED) expect(policy_project.container_registry_access_level).to eq(ProjectFeature::DISABLED)
expect(policy_project.repository.readme.data).to include('# Security Policy Project for')
expect(policy_project.repository.readme.data).to include('## Default branch protection settings')
end end
end end
......
...@@ -3,7 +3,10 @@ import Bold from '~/content_editor/extensions/bold'; ...@@ -3,7 +3,10 @@ import Bold from '~/content_editor/extensions/bold';
import BulletList from '~/content_editor/extensions/bullet_list'; import BulletList from '~/content_editor/extensions/bullet_list';
import Code from '~/content_editor/extensions/code'; import Code from '~/content_editor/extensions/code';
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight'; import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
import Division from '~/content_editor/extensions/division';
import Emoji from '~/content_editor/extensions/emoji'; import Emoji from '~/content_editor/extensions/emoji';
import Figure from '~/content_editor/extensions/figure';
import FigureCaption from '~/content_editor/extensions/figure_caption';
import HardBreak from '~/content_editor/extensions/hard_break'; import HardBreak from '~/content_editor/extensions/hard_break';
import Heading from '~/content_editor/extensions/heading'; import Heading from '~/content_editor/extensions/heading';
import HorizontalRule from '~/content_editor/extensions/horizontal_rule'; import HorizontalRule from '~/content_editor/extensions/horizontal_rule';
...@@ -38,7 +41,10 @@ const tiptapEditor = createTestEditor({ ...@@ -38,7 +41,10 @@ const tiptapEditor = createTestEditor({
BulletList, BulletList,
Code, Code,
CodeBlockHighlight, CodeBlockHighlight,
Division,
Emoji, Emoji,
Figure,
FigureCaption,
HardBreak, HardBreak,
Heading, Heading,
HorizontalRule, HorizontalRule,
...@@ -68,7 +74,10 @@ const { ...@@ -68,7 +74,10 @@ const {
bulletList, bulletList,
code, code,
codeBlock, codeBlock,
division,
emoji, emoji,
figure,
figureCaption,
heading, heading,
hardBreak, hardBreak,
horizontalRule, horizontalRule,
...@@ -95,7 +104,10 @@ const { ...@@ -95,7 +104,10 @@ const {
bulletList: { nodeType: BulletList.name }, bulletList: { nodeType: BulletList.name },
code: { markType: Code.name }, code: { markType: Code.name },
codeBlock: { nodeType: CodeBlockHighlight.name }, codeBlock: { nodeType: CodeBlockHighlight.name },
division: { nodeType: Division.name },
emoji: { markType: Emoji.name }, emoji: { markType: Emoji.name },
figure: { nodeType: Figure.name },
figureCaption: { nodeType: FigureCaption.name },
hardBreak: { nodeType: HardBreak.name }, hardBreak: { nodeType: HardBreak.name },
heading: { nodeType: Heading.name }, heading: { nodeType: Heading.name },
horizontalRule: { nodeType: HorizontalRule.name }, horizontalRule: { nodeType: HorizontalRule.name },
...@@ -533,6 +545,61 @@ this is not really json but just trying out whether this case works or not ...@@ -533,6 +545,61 @@ this is not really json but just trying out whether this case works or not
); );
}); });
it('correctly renders div', () => {
expect(
serialize(
division(paragraph('just a paragraph in a div')),
division(paragraph('just some ', bold('styled'), ' ', italic('content'), ' in a div')),
),
).toBe(
'<div>just a paragraph in a div</div>\n<div>\n\njust some **styled** _content_ in a div\n\n</div>',
);
});
it('correctly renders figure', () => {
expect(
serialize(
figure(
paragraph(image({ src: 'elephant.jpg', alt: 'An elephant at sunset' })),
figureCaption('An elephant at sunset'),
),
),
).toBe(
`
<figure>
![An elephant at sunset](elephant.jpg)
<figcaption>An elephant at sunset</figcaption>
</figure>
`.trim(),
);
});
it('correctly renders figure with styled caption', () => {
expect(
serialize(
figure(
paragraph(image({ src: 'elephant.jpg', alt: 'An elephant at sunset' })),
figureCaption(italic('An elephant at sunset')),
),
),
).toBe(
`
<figure>
![An elephant at sunset](elephant.jpg)
<figcaption>
_An elephant at sunset_
</figcaption>
</figure>
`.trim(),
);
});
it('correctly serializes a table with inline content', () => { it('correctly serializes a table with inline content', () => {
expect( expect(
serialize( serialize(
......
...@@ -33,6 +33,32 @@ ...@@ -33,6 +33,32 @@
* <ruby>漢<rt>ㄏㄢˋ</rt></ruby> * <ruby>漢<rt>ㄏㄢˋ</rt></ruby>
* C<sub>7</sub>H<sub>16</sub> + O<sub>2</sub> → CO<sub>2</sub> + H<sub>2</sub>O * C<sub>7</sub>H<sub>16</sub> + O<sub>2</sub> → CO<sub>2</sub> + H<sub>2</sub>O
* The **Pythagorean theorem** is often expressed as <var>a<sup>2</sup></var> + <var>b<sup>2</sup></var> = <var>c<sup>2</sup></var> * The **Pythagorean theorem** is often expressed as <var>a<sup>2</sup></var> + <var>b<sup>2</sup></var> = <var>c<sup>2</sup></var>
- name: div
markdown: |-
<div>plain text</div>
<div>
just a plain ol' div, not much to _expect_!
</div>
- name: figure
markdown: |-
<figure>
![Elephant at sunset](elephant-sunset.jpg)
<figcaption>An elephant at sunset</figcaption>
</figure>
<figure>
![A crocodile wearing crocs](croc-crocs.jpg)
<figcaption>
A crocodile wearing _crocs_!
</figcaption>
</figure>
- name: link - name: link
markdown: '[GitLab](https://gitlab.com)' markdown: '[GitLab](https://gitlab.com)'
- name: attachment_link - name: attachment_link
......
...@@ -1755,14 +1755,26 @@ RSpec.describe User do ...@@ -1755,14 +1755,26 @@ RSpec.describe User do
end end
describe '#manageable_groups' do describe '#manageable_groups' do
it 'includes all the namespaces the user can manage' do shared_examples 'manageable groups examples' do
expect(user.manageable_groups).to contain_exactly(group, subgroup) it 'includes all the namespaces the user can manage' do
expect(user.manageable_groups).to contain_exactly(group, subgroup)
end
it 'does not include duplicates if a membership was added for the subgroup' do
subgroup.add_owner(user)
expect(user.manageable_groups).to contain_exactly(group, subgroup)
end
end end
it 'does not include duplicates if a membership was added for the subgroup' do it_behaves_like 'manageable groups examples'
subgroup.add_owner(user)
context 'when feature flag :linear_user_manageable_groups is disabled' do
before do
stub_feature_flags(linear_user_manageable_groups: false)
end
expect(user.manageable_groups).to contain_exactly(group, subgroup) it_behaves_like 'manageable groups examples'
end end
end end
......
...@@ -601,6 +601,18 @@ RSpec.describe Projects::CreateService, '#execute' do ...@@ -601,6 +601,18 @@ RSpec.describe Projects::CreateService, '#execute' do
MARKDOWN MARKDOWN
end end
end end
context 'and readme_template is specified' do
before do
opts[:readme_template] = "# GitLab\nThis is customized template."
end
it_behaves_like 'creates README.md'
it 'creates README.md with specified template' do
expect(project.repository.readme.data).to include('This is customized template.')
end
end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment