Commit 22a0d312 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 37eff29d
<svg width="100" height="32" xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero" fill="none"><path fill="#8C929D" d="M67.67 8.11h-2.06l.009 15.364h8.348v-1.9H67.68l-.01-13.465zM81.913 20.778a3.517 3.517 0 01-2.553 1.078c-1.57 0-2.203-.775-2.203-1.787 0-1.522 1.059-2.25 3.309-2.25.487.002.974.04 1.456.113v2.846h-.01zm-2.137-9.313a6.826 6.826 0 00-4.387 1.579l.728 1.267c.841-.492 1.872-.983 3.356-.983 1.693 0 2.44.87 2.44 2.326v.747a9.4 9.4 0 00-1.428-.114c-3.612 0-5.446 1.267-5.446 3.914 0 2.374 1.456 3.565 3.659 3.565 1.484 0 2.912-.68 3.404-1.787l.378 1.503h1.456v-7.866c-.01-2.487-1.087-4.151-4.16-4.151zM90.587 21.926c-.776 0-1.456-.094-1.967-.33v-7.102c.7-.586 1.57-1.011 2.676-1.011 1.995 0 2.76 1.408 2.76 3.687 0 3.234-1.238 4.756-3.47 4.756m.87-10.457a3.775 3.775 0 00-2.836 1.257V10.74l-.01-2.629h-2.013l.01 14.987c1.01.425 2.391.652 3.895.652 3.848 0 5.701-2.458 5.701-6.704-.01-3.356-1.72-5.578-4.746-5.578M45.228 9.776c1.825 0 3.006.605 3.772 1.22l.889-1.541c-1.2-1.06-2.827-1.627-4.567-1.627-4.387 0-7.46 2.676-7.46 8.075 0 5.654 3.319 7.857 7.11 7.857a12.083 12.083 0 004.577-.888L49.5 16.83v-1.9h-5.63v1.9h3.594l.047 4.586c-.473.236-1.286.425-2.392.425-3.045 0-5.087-1.92-5.087-5.957-.01-4.113 2.1-6.108 5.19-6.108M59.744 8.107H57.73l.01 2.582v8.916c0 2.487 1.078 4.15 4.15 4.15.416.002.83-.036 1.24-.113v-1.806c-.31.047-.624.07-.937.066-1.692 0-2.44-.87-2.44-2.326v-6.145h3.376v-1.683h-3.373l-.009-3.64h-.003zM52.608 23.474h2.014V11.75h-2.014zM52.608 10.133h2.014V8.119h-2.014z"/><path d="M31.864 17.907l-1.788-5.496-3.538-10.9a.612.612 0 00-1.16 0L21.84 12.406H10.085L6.547 1.512a.612.612 0 00-1.16 0L1.855 12.405.066 17.907c-.162.5.015 1.05.44 1.36L15.963 30.5l15.456-11.233a1.22 1.22 0 00.446-1.36" fill="#FC6D26"/><path d="M15.966 30.49l5.875-18.086H10.09z" fill="#E24329"/><path d="M15.962 30.49l-5.877-18.086H1.859z" fill="#FC6D26"/><path d="M1.852 12.41L.063 17.906c-.162.5.015 1.05.441 1.36L15.959 30.5 1.852 12.41z" fill="#FCA326"/><path d="M1.854 12.41h8.237L6.546 1.517a.612.612 0 00-1.16 0L1.854 12.41z" fill="#E24329"/><path d="M15.966 30.49l5.875-18.086h8.236z" fill="#FC6D26"/><path d="M30.074 12.41l1.79 5.496a1.219 1.219 0 01-.44 1.36L15.966 30.49l14.107-18.08z" fill="#FCA326"/><path d="M30.079 12.41H21.84L25.38 1.517a.612.612 0 011.16 0l3.539 10.893z" fill="#E24329"/></g></svg>
\ No newline at end of file
...@@ -104,6 +104,7 @@ export default { ...@@ -104,6 +104,7 @@ export default {
visibilityLevel: visibilityOptions.PUBLIC, visibilityLevel: visibilityOptions.PUBLIC,
issuesAccessLevel: 20, issuesAccessLevel: 20,
repositoryAccessLevel: 20, repositoryAccessLevel: 20,
forkingAccessLevel: 20,
mergeRequestsAccessLevel: 20, mergeRequestsAccessLevel: 20,
buildsAccessLevel: 20, buildsAccessLevel: 20,
wikiAccessLevel: 20, wikiAccessLevel: 20,
...@@ -300,6 +301,19 @@ export default { ...@@ -300,6 +301,19 @@ export default {
name="project[project_feature_attributes][merge_requests_access_level]" name="project[project_feature_attributes][merge_requests_access_level]"
/> />
</project-setting-row> </project-setting-row>
<project-setting-row
:label="s__('ProjectSettings|Forks')"
:help-text="
s__('ProjectSettings|Allow users to make copies of your repository to a new project')
"
>
<project-feature-setting
v-model="forkingAccessLevel"
:options="featureAccessLevelOptions"
:disabled-input="!repositoryEnabled"
name="project[project_feature_attributes][forking_access_level]"
/>
</project-setting-row>
<project-setting-row <project-setting-row
:label="s__('ProjectSettings|Pipelines')" :label="s__('ProjectSettings|Pipelines')"
:help-text="s__('ProjectSettings|Build, test, and deploy your changes')" :help-text="s__('ProjectSettings|Build, test, and deploy your changes')"
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
pre { pre {
padding: 10px 0; padding: 10px 0;
border: 0; border: 0;
border-radius: 0 0 $border-radius-default $border-radius-default; border-radius: 0 0 $border-radius-default;
font-family: $monospace-font; font-family: $monospace-font;
font-size: $code-font-size; font-size: $code-font-size;
line-height: 1.5; line-height: 1.5;
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
line-height: $code-line-height; line-height: $code-line-height;
color: $gl-text-color; color: $gl-text-color;
margin: 20px; margin: 20px;
font-weight: 200; font-weight: $gl-font-weight-normal;
.gl-snippet-icon { .gl-snippet-icon {
display: inline-block; display: inline-block;
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
.file-content.code { .file-content.code {
border: $border-style; border: $border-style;
border-radius: 0 0 4px 4px; border-radius: 0 0 $border-radius-default $border-radius-default;
display: flex; display: flex;
box-shadow: none; box-shadow: none;
margin: 0; margin: 0;
...@@ -45,6 +45,7 @@ ...@@ -45,6 +45,7 @@
overflow-x: auto; overflow-x: auto;
pre { pre {
height: 100%;
padding: 10px; padding: 10px;
border: 0; border: 0;
border-radius: 0; border-radius: 0;
...@@ -110,17 +111,13 @@ ...@@ -110,17 +111,13 @@
} }
} }
.gitlab-logo { .gitlab-logo-wrapper {
display: inline-block; padding-left: $gl-padding-8;
padding-left: 5px; position: relative;
text-decoration: none; top: 2px;
color: $gl-text-color-secondary;
.logo-text { .gitlab-logo {
background: image_url('ext_snippet_icons/logo.png') no-repeat left center; height: 18px;
background-size: 18px;
font-weight: $gl-font-weight-normal;
padding-left: 24px;
} }
} }
} }
...@@ -128,7 +125,7 @@ ...@@ -128,7 +125,7 @@
img, img,
.gl-snippet-icon { .gl-snippet-icon {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: text-bottom;
} }
} }
...@@ -136,7 +133,7 @@ ...@@ -136,7 +133,7 @@
a.btn { a.btn {
background-color: $white-light; background-color: $white-light;
text-decoration: none; text-decoration: none;
padding: 7px 9px; padding: 8px 9px;
border: $border-style; border: $border-style;
border-right: 0; border-right: 0;
...@@ -147,11 +144,11 @@ ...@@ -147,11 +144,11 @@
} }
&:first-child { &:first-child {
border-radius: 3px 0 0 3px; border-radius: $border-radius-default 0 0 $border-radius-default;
} }
&:last-child { &:last-child {
border-radius: 0 3px 3px 0; border-radius: 0 $border-radius-default $border-radius-default 0;
border-right: $border-style; border-right: $border-style;
} }
} }
......
...@@ -9,6 +9,7 @@ class Projects::ForksController < Projects::ApplicationController ...@@ -9,6 +9,7 @@ class Projects::ForksController < Projects::ApplicationController
before_action :require_non_empty_project before_action :require_non_empty_project
before_action :authorize_download_code! before_action :authorize_download_code!
before_action :authenticate_user!, only: [:new, :create] before_action :authenticate_user!, only: [:new, :create]
before_action :authorize_fork_project!, only: [:new, :create]
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def index def index
...@@ -61,6 +62,8 @@ class Projects::ForksController < Projects::ApplicationController ...@@ -61,6 +62,8 @@ class Projects::ForksController < Projects::ApplicationController
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
private
def whitelist_query_limiting def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42335') Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42335')
end end
......
...@@ -391,6 +391,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -391,6 +391,7 @@ class ProjectsController < Projects::ApplicationController
project_feature_attributes: %i[ project_feature_attributes: %i[
builds_access_level builds_access_level
issues_access_level issues_access_level
forking_access_level
merge_requests_access_level merge_requests_access_level
repository_access_level repository_access_level
snippets_access_level snippets_access_level
......
...@@ -17,6 +17,9 @@ module Types ...@@ -17,6 +17,9 @@ module Types
group.avatar_url(only_path: false) group.avatar_url(only_path: false)
end end
field :mentions_disabled, GraphQL::BOOLEAN_TYPE, null: true,
description: 'Indicates if a group is disabled from getting mentioned'
field :parent, GroupType, null: true, field :parent, GroupType, null: true,
description: 'Parent group', description: 'Parent group',
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, obj.parent_id).find } resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, obj.parent_id).find }
......
...@@ -563,6 +563,7 @@ module ProjectsHelper ...@@ -563,6 +563,7 @@ module ProjectsHelper
requestAccessEnabled: !!project.request_access_enabled, requestAccessEnabled: !!project.request_access_enabled,
issuesAccessLevel: feature.issues_access_level, issuesAccessLevel: feature.issues_access_level,
repositoryAccessLevel: feature.repository_access_level, repositoryAccessLevel: feature.repository_access_level,
forkingAccessLevel: feature.forking_access_level,
mergeRequestsAccessLevel: feature.merge_requests_access_level, mergeRequestsAccessLevel: feature.merge_requests_access_level,
buildsAccessLevel: feature.builds_access_level, buildsAccessLevel: feature.builds_access_level,
wikiAccessLevel: feature.wiki_access_level, wikiAccessLevel: feature.wiki_access_level,
......
...@@ -50,6 +50,10 @@ module ProjectFeaturesCompatibility ...@@ -50,6 +50,10 @@ module ProjectFeaturesCompatibility
write_feature_attribute_string(:merge_requests_access_level, value) write_feature_attribute_string(:merge_requests_access_level, value)
end end
def forking_access_level=(value)
write_feature_attribute_string(:forking_access_level, value)
end
def issues_access_level=(value) def issues_access_level=(value)
write_feature_attribute_string(:issues_access_level, value) write_feature_attribute_string(:issues_access_level, value)
end end
......
...@@ -128,7 +128,7 @@ module ErrorTracking ...@@ -128,7 +128,7 @@ module ErrorTracking
# -> # ->
# http://HOST/ORG/PROJECT # http://HOST/ORG/PROJECT
def self.extract_sentry_external_url(url) def self.extract_sentry_external_url(url)
url.sub('api/0/projects/', '') url&.sub('api/0/projects/', '')
end end
def api_host def api_host
......
...@@ -317,10 +317,12 @@ class Project < ApplicationRecord ...@@ -317,10 +317,12 @@ class Project < ApplicationRecord
accepts_nested_attributes_for :metrics_setting, update_only: true, allow_destroy: true accepts_nested_attributes_for :metrics_setting, update_only: true, allow_destroy: true
accepts_nested_attributes_for :grafana_integration, update_only: true, allow_destroy: true accepts_nested_attributes_for :grafana_integration, update_only: true, allow_destroy: true
delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
:issues_enabled?, :pages_enabled?, :public_pages?, :private_pages?, :merge_requests_enabled?, :forking_enabled?, :issues_enabled?,
:merge_requests_access_level, :issues_access_level, :wiki_access_level, :pages_enabled?, :public_pages?, :private_pages?,
:snippets_access_level, :builds_access_level, :repository_access_level, :merge_requests_access_level, :forking_access_level, :issues_access_level,
:wiki_access_level, :snippets_access_level, :builds_access_level,
:repository_access_level,
to: :project_feature, allow_nil: true to: :project_feature, allow_nil: true
delegate :scheduled?, :started?, :in_progress?, :failed?, :finished?, delegate :scheduled?, :started?, :in_progress?, :failed?, :finished?,
prefix: :import, to: :import_state, allow_nil: true prefix: :import, to: :import_state, allow_nil: true
......
...@@ -22,7 +22,7 @@ class ProjectFeature < ApplicationRecord ...@@ -22,7 +22,7 @@ class ProjectFeature < ApplicationRecord
ENABLED = 20 ENABLED = 20
PUBLIC = 30 PUBLIC = 30
FEATURES = %i(issues merge_requests wiki snippets builds repository pages).freeze FEATURES = %i(issues forking merge_requests wiki snippets builds repository pages).freeze
PRIVATE_FEATURES_MIN_ACCESS_LEVEL = { merge_requests: Gitlab::Access::REPORTER }.freeze PRIVATE_FEATURES_MIN_ACCESS_LEVEL = { merge_requests: Gitlab::Access::REPORTER }.freeze
PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT = { repository: Gitlab::Access::REPORTER }.freeze PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT = { repository: Gitlab::Access::REPORTER }.freeze
STRING_OPTIONS = HashWithIndifferentAccess.new({ STRING_OPTIONS = HashWithIndifferentAccess.new({
...@@ -92,6 +92,7 @@ class ProjectFeature < ApplicationRecord ...@@ -92,6 +92,7 @@ class ProjectFeature < ApplicationRecord
default_value_for :builds_access_level, value: ENABLED, allows_nil: false default_value_for :builds_access_level, value: ENABLED, allows_nil: false
default_value_for :issues_access_level, value: ENABLED, allows_nil: false default_value_for :issues_access_level, value: ENABLED, allows_nil: false
default_value_for :forking_access_level, value: ENABLED, allows_nil: false
default_value_for :merge_requests_access_level, value: ENABLED, allows_nil: false default_value_for :merge_requests_access_level, value: ENABLED, allows_nil: false
default_value_for :snippets_access_level, value: ENABLED, allows_nil: false default_value_for :snippets_access_level, value: ENABLED, allows_nil: false
default_value_for :wiki_access_level, value: ENABLED, allows_nil: false default_value_for :wiki_access_level, value: ENABLED, allows_nil: false
...@@ -132,6 +133,10 @@ class ProjectFeature < ApplicationRecord ...@@ -132,6 +133,10 @@ class ProjectFeature < ApplicationRecord
merge_requests_access_level > DISABLED merge_requests_access_level > DISABLED
end end
def forking_enabled?
forking_access_level > DISABLED
end
def issues_enabled? def issues_enabled?
issues_access_level > DISABLED issues_access_level > DISABLED
end end
......
...@@ -83,6 +83,11 @@ class ProjectPolicy < BasePolicy ...@@ -83,6 +83,11 @@ class ProjectPolicy < BasePolicy
project.merge_requests_allowing_push_to_user(user).any? project.merge_requests_allowing_push_to_user(user).any?
end end
with_scope :subject
condition(:forking_allowed) do
@subject.feature_available?(:forking, @user)
end
with_scope :global with_scope :global
condition(:mirror_available, score: 0) do condition(:mirror_available, score: 0) do
::Gitlab::CurrentSettings.current_application_settings.mirror_available ::Gitlab::CurrentSettings.current_application_settings.mirror_available
...@@ -203,7 +208,6 @@ class ProjectPolicy < BasePolicy ...@@ -203,7 +208,6 @@ class ProjectPolicy < BasePolicy
enable :download_code enable :download_code
enable :read_statistics enable :read_statistics
enable :download_wiki_code enable :download_wiki_code
enable :fork_project
enable :create_project_snippet enable :create_project_snippet
enable :update_issue enable :update_issue
enable :reopen_issue enable :reopen_issue
...@@ -232,12 +236,15 @@ class ProjectPolicy < BasePolicy ...@@ -232,12 +236,15 @@ class ProjectPolicy < BasePolicy
enable :public_access enable :public_access
enable :guest_access enable :guest_access
enable :fork_project
enable :build_download_code enable :build_download_code
enable :build_read_container_image enable :build_read_container_image
enable :request_access enable :request_access
end end
rule { can?(:download_code) & forking_allowed }.policy do
enable :fork_project
end
rule { owner | admin | guest | group_member }.prevent :request_access rule { owner | admin | guest | group_member }.prevent :request_access
rule { ~request_access_enabled }.prevent :request_access rule { ~request_access_enabled }.prevent :request_access
......
...@@ -60,9 +60,7 @@ module SystemNoteService ...@@ -60,9 +60,7 @@ module SystemNoteService
# #
# Returns the created Note object # Returns the created Note object
def change_due_date(noteable, project, author, due_date) def change_due_date(noteable, project, author, due_date)
body = due_date ? "changed due date to #{due_date.to_s(:long)}" : 'removed due date' ::SystemNotes::TimeTrackingService.new(noteable: noteable, project: project, author: author).change_due_date(due_date)
create_note(NoteSummary.new(noteable, project, author, body, action: 'due_date'))
end end
# Called when the estimated time of a Noteable is changed # Called when the estimated time of a Noteable is changed
...@@ -80,14 +78,7 @@ module SystemNoteService ...@@ -80,14 +78,7 @@ module SystemNoteService
# #
# Returns the created Note object # Returns the created Note object
def change_time_estimate(noteable, project, author) def change_time_estimate(noteable, project, author)
parsed_time = Gitlab::TimeTrackingFormatter.output(noteable.time_estimate) ::SystemNotes::TimeTrackingService.new(noteable: noteable, project: project, author: author).change_time_estimate
body = if noteable.time_estimate == 0
"removed time estimate"
else
"changed time estimate to #{parsed_time}"
end
create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
end end
# Called when the spent time of a Noteable is changed # Called when the spent time of a Noteable is changed
...@@ -105,21 +96,7 @@ module SystemNoteService ...@@ -105,21 +96,7 @@ module SystemNoteService
# #
# Returns the created Note object # Returns the created Note object
def change_time_spent(noteable, project, author) def change_time_spent(noteable, project, author)
time_spent = noteable.time_spent ::SystemNotes::TimeTrackingService.new(noteable: noteable, project: project, author: author).change_time_spent
if time_spent == :reset
body = "removed time spent"
else
spent_at = noteable.spent_at
parsed_time = Gitlab::TimeTrackingFormatter.output(time_spent.abs)
action = time_spent > 0 ? 'added' : 'subtracted'
text_parts = ["#{action} #{parsed_time} of time spent"]
text_parts << "at #{spent_at}" if spent_at
body = text_parts.join(' ')
end
create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
end end
def change_status(noteable, project, author, status, source = nil) def change_status(noteable, project, author, status, source = nil)
......
# frozen_string_literal: true
module SystemNotes
class TimeTrackingService < ::SystemNotes::BaseService
# Called when the due_date of a Noteable is changed
#
# due_date - Due date being assigned, or nil
#
# Example Note text:
#
# "removed due date"
#
# "changed due date to September 20, 2018"
#
# Returns the created Note object
def change_due_date(due_date)
body = due_date ? "changed due date to #{due_date.to_s(:long)}" : 'removed due date'
create_note(NoteSummary.new(noteable, project, author, body, action: 'due_date'))
end
# Called when the estimated time of a Noteable is changed
#
# time_estimate - Estimated time
#
# Example Note text:
#
# "removed time estimate"
#
# "changed time estimate to 3d 5h"
#
# Returns the created Note object
def change_time_estimate
parsed_time = Gitlab::TimeTrackingFormatter.output(noteable.time_estimate)
body = if noteable.time_estimate == 0
"removed time estimate"
else
"changed time estimate to #{parsed_time}"
end
create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
end
# Called when the spent time of a Noteable is changed
#
# time_spent - Spent time
#
# Example Note text:
#
# "removed time spent"
#
# "added 2h 30m of time spent"
#
# Returns the created Note object
def change_time_spent
time_spent = noteable.time_spent
if time_spent == :reset
body = "removed time spent"
else
spent_at = noteable.spent_at
parsed_time = Gitlab::TimeTrackingFormatter.output(time_spent.abs)
action = time_spent > 0 ? 'added' : 'subtracted'
text_parts = ["#{action} #{parsed_time} of time spent"]
text_parts << "at #{spent_at}" if spent_at
body = text_parts.join(' ')
end
create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
end
end
end
...@@ -10,10 +10,8 @@ ...@@ -10,10 +10,8 @@
%small %small
= number_to_human_size(blob.raw_size) = number_to_human_size(blob.raw_size)
%a.gitlab-logo{ href: url_for(only_path: false, overwrite_params: nil), title: 'view on gitlab' } %a.gitlab-logo-wrapper{ href: url_for(only_path: false, overwrite_params: nil), title: 'view on gitlab' }
on &nbsp; %img.gitlab-logo{ src: image_url('ext_snippet_icons/logo.svg'), alt: "GitLab logo" }
%span.logo-text
GitLab
.file-actions.d-none.d-sm-block .file-actions.d-none.d-sm-block
.btn-group{ role: "group" }< .btn-group{ role: "group" }<
......
---
title: Add an option to configure forking restriction
merge_request: 17988
author:
type: added
---
title: Add ability to create an issue in an epic
merge_request: 22833
author:
type: added
---
title: Fix embedded snippets UI polish issues
merge_request: !22637
author:
type: changed
---
title: Require group owner to have linked SAML before enabling Group Managed Accounts
merge_request: 21721
author:
type: fixed
---
title: Fix extracting Sentry external URL when URL is nil
merge_request: 23162
author:
type: fixed
---
title: Expose mentions_disabled value via group API
merge_request: 23070
author: Fabio Huser
type: added
# frozen_string_literal: true
class AddForkingAccessLevelToProjectFeature < ActiveRecord::Migration[5.2]
DOWNTIME = false
def change
add_column :project_features, :forking_access_level, :integer
end
end
...@@ -3167,6 +3167,7 @@ ActiveRecord::Schema.define(version: 2020_01_14_204949) do ...@@ -3167,6 +3167,7 @@ ActiveRecord::Schema.define(version: 2020_01_14_204949) do
t.datetime "updated_at" t.datetime "updated_at"
t.integer "repository_access_level", default: 20, null: false t.integer "repository_access_level", default: 20, null: false
t.integer "pages_access_level", null: false t.integer "pages_access_level", null: false
t.integer "forking_access_level"
t.index ["project_id"], name: "index_project_features_on_project_id", unique: true t.index ["project_id"], name: "index_project_features_on_project_id", unique: true
end end
......
...@@ -2594,6 +2594,11 @@ type Group { ...@@ -2594,6 +2594,11 @@ type Group {
""" """
lfsEnabled: Boolean lfsEnabled: Boolean
"""
Indicates if a group is disabled from getting mentioned
"""
mentionsDisabled: Boolean
""" """
Name of the namespace Name of the namespace
""" """
......
...@@ -3460,6 +3460,20 @@ ...@@ -3460,6 +3460,20 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "mentionsDisabled",
"description": "Indicates if a group is disabled from getting mentioned",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "name", "name": "name",
"description": "Name of the namespace", "description": "Name of the namespace",
......
...@@ -393,6 +393,7 @@ Autogenerated return type of EpicTreeReorder ...@@ -393,6 +393,7 @@ Autogenerated return type of EpicTreeReorder
| `userPermissions` | GroupPermissions! | Permissions for the current user on the resource | | `userPermissions` | GroupPermissions! | Permissions for the current user on the resource |
| `webUrl` | String! | Web URL of the group | | `webUrl` | String! | Web URL of the group |
| `avatarUrl` | String | Avatar URL of the group | | `avatarUrl` | String | Avatar URL of the group |
| `mentionsDisabled` | Boolean | Indicates if a group is disabled from getting mentioned |
| `parent` | Group | Parent group | | `parent` | Group | Parent group |
| `epicsEnabled` | Boolean | Indicates if Epics are enabled for namespace | | `epicsEnabled` | Boolean | Indicates if Epics are enabled for namespace |
| `groupTimelogsEnabled` | Boolean | Indicates if Group timelogs are enabled for namespace | | `groupTimelogsEnabled` | Boolean | Indicates if Group timelogs are enabled for namespace |
......
...@@ -40,6 +40,7 @@ GET /groups ...@@ -40,6 +40,7 @@ GET /groups
"auto_devops_enabled": null, "auto_devops_enabled": null,
"subgroup_creation_level": "owner", "subgroup_creation_level": "owner",
"emails_disabled": null, "emails_disabled": null,
"mentions_disabled": null,
"lfs_enabled": true, "lfs_enabled": true,
"avatar_url": "http://localhost:3000/uploads/group/avatar/1/foo.jpg", "avatar_url": "http://localhost:3000/uploads/group/avatar/1/foo.jpg",
"web_url": "http://localhost:3000/groups/foo-bar", "web_url": "http://localhost:3000/groups/foo-bar",
...@@ -73,6 +74,7 @@ GET /groups?statistics=true ...@@ -73,6 +74,7 @@ GET /groups?statistics=true
"auto_devops_enabled": null, "auto_devops_enabled": null,
"subgroup_creation_level": "owner", "subgroup_creation_level": "owner",
"emails_disabled": null, "emails_disabled": null,
"mentions_disabled": null,
"lfs_enabled": true, "lfs_enabled": true,
"avatar_url": "http://localhost:3000/uploads/group/avatar/1/foo.jpg", "avatar_url": "http://localhost:3000/uploads/group/avatar/1/foo.jpg",
"web_url": "http://localhost:3000/groups/foo-bar", "web_url": "http://localhost:3000/groups/foo-bar",
...@@ -144,6 +146,7 @@ GET /groups/:id/subgroups ...@@ -144,6 +146,7 @@ GET /groups/:id/subgroups
"auto_devops_enabled": null, "auto_devops_enabled": null,
"subgroup_creation_level": "owner", "subgroup_creation_level": "owner",
"emails_disabled": null, "emails_disabled": null,
"mentions_disabled": null,
"lfs_enabled": true, "lfs_enabled": true,
"avatar_url": "http://gitlab.example.com/uploads/group/avatar/1/foo.jpg", "avatar_url": "http://gitlab.example.com/uploads/group/avatar/1/foo.jpg",
"web_url": "http://gitlab.example.com/groups/foo-bar", "web_url": "http://gitlab.example.com/groups/foo-bar",
...@@ -486,6 +489,7 @@ Parameters: ...@@ -486,6 +489,7 @@ Parameters:
| `auto_devops_enabled` | boolean | no | Default to Auto DevOps pipeline for all projects within this group. | | `auto_devops_enabled` | boolean | no | Default to Auto DevOps pipeline for all projects within this group. |
| `subgroup_creation_level` | integer | no | Allowed to create subgroups. Can be `owner` (Owners), or `maintainer` (Maintainers). | | `subgroup_creation_level` | integer | no | Allowed to create subgroups. Can be `owner` (Owners), or `maintainer` (Maintainers). |
| `emails_disabled` | boolean | no | Disable email notifications | | `emails_disabled` | boolean | no | Disable email notifications |
| `mentions_disabled` | boolean | no | Disable the capability of a group from getting mentioned |
| `lfs_enabled` | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group. | | `lfs_enabled` | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group. |
| `request_access_enabled` | boolean | no | Allow users to request member access. | | `request_access_enabled` | boolean | no | Allow users to request member access. |
| `parent_id` | integer | no | The parent group ID for creating nested group. | | `parent_id` | integer | no | The parent group ID for creating nested group. |
...@@ -531,6 +535,7 @@ PUT /groups/:id ...@@ -531,6 +535,7 @@ PUT /groups/:id
| `auto_devops_enabled` | boolean | no | Default to Auto DevOps pipeline for all projects within this group. | | `auto_devops_enabled` | boolean | no | Default to Auto DevOps pipeline for all projects within this group. |
| `subgroup_creation_level` | integer | no | Allowed to create subgroups. Can be `owner` (Owners), or `maintainer` (Maintainers). | | `subgroup_creation_level` | integer | no | Allowed to create subgroups. Can be `owner` (Owners), or `maintainer` (Maintainers). |
| `emails_disabled` | boolean | no | Disable email notifications | | `emails_disabled` | boolean | no | Disable email notifications |
| `mentions_disabled` | boolean | no | Disable the capability of a group from getting mentioned |
| `lfs_enabled` (optional) | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group. | | `lfs_enabled` (optional) | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group. |
| `request_access_enabled` | boolean | no | Allow users to request member access. | | `request_access_enabled` | boolean | no | Allow users to request member access. |
| `file_template_project_id` | integer | no | **(PREMIUM)** The ID of a project to load custom file templates from. | | `file_template_project_id` | integer | no | **(PREMIUM)** The ID of a project to load custom file templates from. |
......
...@@ -17,7 +17,7 @@ description: 'Learn how to contribute to GitLab.' ...@@ -17,7 +17,7 @@ description: 'Learn how to contribute to GitLab.'
- [GitLab core team & GitLab Inc. contribution process](https://gitlab.com/gitlab-org/gitlab/blob/master/PROCESS.md) - [GitLab core team & GitLab Inc. contribution process](https://gitlab.com/gitlab-org/gitlab/blob/master/PROCESS.md)
- [Generate a changelog entry with `bin/changelog`](changelog.md) - [Generate a changelog entry with `bin/changelog`](changelog.md)
- [Code review guidelines](code_review.md) for reviewing code and having code reviewed - [Code review guidelines](code_review.md) for reviewing code and having code reviewed
- [Database review guidelines](database_review.md) for reviewing database-related changes and complex SQL queries - [Database review guidelines](database_review.md) for reviewing database-related changes and complex SQL queries, and having them reviewed
- [Pipelines for the GitLab project](pipelines.md) - [Pipelines for the GitLab project](pipelines.md)
- [Guidelines for implementing Enterprise Edition features](ee_features.md) - [Guidelines for implementing Enterprise Edition features](ee_features.md)
- [Security process for developers](https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#security-releases-critical-non-critical-as-a-developer) - [Security process for developers](https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#security-releases-critical-non-critical-as-a-developer)
......
...@@ -204,6 +204,25 @@ and give all group members access to the project at once. ...@@ -204,6 +204,25 @@ and give all group members access to the project at once.
Alternatively, you can [lock the sharing with group feature](#share-with-group-lock). Alternatively, you can [lock the sharing with group feature](#share-with-group-lock).
## Sharing a group with another group
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/18328) in GitLab 12.7.
Similarly to [sharing a project with a group](#sharing-a-project-with-a-group),
you can share a group with another group to give direct group members access
to the shared group. This is not valid for inherited members.
To share a given group, for example, 'Frontend' with another group, for example,
'Engineering':
1. Navigate to your 'Frontend' group page and use the left navigation menu to go
to your group **Members**.
1. Select the **Invite group** tab.
1. Add 'Engineering' with the maximum access level of your choice.
1. Click **Invite**.
All the members of the 'Engineering' group will have been added to 'Frontend'.
## Manage group memberships via LDAP ## Manage group memberships via LDAP
In GitLab Enterprise Edition, it is possible to manage GitLab group memberships using LDAP groups. In GitLab Enterprise Edition, it is possible to manage GitLab group memberships using LDAP groups.
......
...@@ -43,6 +43,7 @@ Use the switches to enable or disable the following features: ...@@ -43,6 +43,7 @@ Use the switches to enable or disable the following features:
| **Issues** | ✓ | Activates the GitLab issues tracker | | **Issues** | ✓ | Activates the GitLab issues tracker |
| **Repository** | ✓ | Enables [repository](../repository/) functionality | | **Repository** | ✓ | Enables [repository](../repository/) functionality |
| **Merge Requests** | ✓ | Enables [merge request](../merge_requests/) functionality; also see [Merge request settings](#merge-request-settings) | | **Merge Requests** | ✓ | Enables [merge request](../merge_requests/) functionality; also see [Merge request settings](#merge-request-settings) |
| **Forks** | ✓ | Enables [forking](../index.md#fork-a-project) functionality |
| **Pipelines** | ✓ | Enables [CI/CD](../../../ci/README.md) functionality | | **Pipelines** | ✓ | Enables [CI/CD](../../../ci/README.md) functionality |
| **Container Registry** | | Activates a [registry](../../packages/container_registry/) for your docker images | | **Container Registry** | | Activates a [registry](../../packages/container_registry/) for your docker images |
| **Git Large File Storage** | | Enables the use of [large files](../../../administration/lfs/manage_large_binaries_with_git_lfs.md#git-lfs) | | **Git Large File Storage** | | Enables the use of [large files](../../../administration/lfs/manage_large_binaries_with_git_lfs.md#git-lfs) |
......
...@@ -414,6 +414,7 @@ module API ...@@ -414,6 +414,7 @@ module API
expose :auto_devops_enabled expose :auto_devops_enabled
expose :subgroup_creation_level_str, as: :subgroup_creation_level expose :subgroup_creation_level_str, as: :subgroup_creation_level
expose :emails_disabled expose :emails_disabled
expose :mentions_disabled
expose :lfs_enabled?, as: :lfs_enabled expose :lfs_enabled?, as: :lfs_enabled
expose :avatar_url do |group, options| expose :avatar_url do |group, options|
group.avatar_url(only_path: false) group.avatar_url(only_path: false)
......
...@@ -18,6 +18,7 @@ module API ...@@ -18,6 +18,7 @@ module API
optional :auto_devops_enabled, type: Boolean, desc: 'Default to Auto DevOps pipeline for all projects within this group' optional :auto_devops_enabled, type: Boolean, desc: 'Default to Auto DevOps pipeline for all projects within this group'
optional :subgroup_creation_level, type: String, values: ::Gitlab::Access.subgroup_creation_string_values, desc: 'Allowed to create subgroups', as: :subgroup_creation_level_str optional :subgroup_creation_level, type: String, values: ::Gitlab::Access.subgroup_creation_string_values, desc: 'Allowed to create subgroups', as: :subgroup_creation_level_str
optional :emails_disabled, type: Boolean, desc: 'Disable email notifications' optional :emails_disabled, type: Boolean, desc: 'Disable email notifications'
optional :mentions_disabled, type: Boolean, desc: 'Disable a group from getting mentioned'
optional :lfs_enabled, type: Boolean, desc: 'Enable/disable LFS for the projects in this group' optional :lfs_enabled, type: Boolean, desc: 'Enable/disable LFS for the projects in this group'
optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access' optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
end end
......
...@@ -35,6 +35,12 @@ module Gitlab ...@@ -35,6 +35,12 @@ module Gitlab
message: 'key may not be used with `rules`' message: 'key may not be used with `rules`'
}, },
if: :has_rules? if: :has_rules?
validates :config,
disallowed_keys: {
in: %i[release],
message: 'release features are not enabled'
},
unless: -> { Feature.enabled?(:ci_release_generation, default_enabled: false) }
with_options allow_nil: true do with_options allow_nil: true do
validates :allow_failure, boolean: true validates :allow_failure, boolean: true
......
...@@ -3,7 +3,14 @@ ...@@ -3,7 +3,14 @@
module Gitlab module Gitlab
module ImportExport module ImportExport
class AttributeCleaner class AttributeCleaner
ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES + %w[group_id commit_id discussion_id custom_attributes] ALLOWED_REFERENCES = [
*ProjectRelationFactory::PROJECT_REFERENCES,
*ProjectRelationFactory::USER_REFERENCES,
'group_id',
'commit_id',
'discussion_id',
'custom_attributes'
].freeze
PROHIBITED_REFERENCES = Regexp.union(/\Acached_markdown_version\Z/, /_id\Z/, /_ids\Z/, /_html\Z/, /attributes/).freeze PROHIBITED_REFERENCES = Regexp.union(/\Acached_markdown_version\Z/, /_id\Z/, /_ids\Z/, /_html\Z/, /attributes/).freeze
def self.clean(*args) def self.clean(*args)
......
# frozen_string_literal: true
module Gitlab
module ImportExport
# Base class for Group & Project Object Builders.
# This class is not intended to be used on its own but
# rather inherited from.
#
# Cache keeps 1000 entries at most, 1000 is chosen based on:
# - one cache entry uses around 0.5K memory, 1000 items uses around 500K.
# (leave some buffer it should be less than 1M). It is afforable cost for project import.
# - for projects in Gitlab.com, it seems 1000 entries for labels/milestones is enough.
# For example, gitlab has ~970 labels and 26 milestones.
LRU_CACHE_SIZE = 1000
class BaseObjectBuilder
def self.build(*args)
new(*args).find
end
def initialize(klass, attributes)
@klass = klass.ancestors.include?(Label) ? Label : klass
@attributes = attributes
if Gitlab::SafeRequestStore.active?
@lru_cache = cache_from_request_store
@cache_key = [klass, attributes]
end
end
def find
find_with_cache do
find_object || klass.create(prepare_attributes)
end
end
protected
def where_clauses
raise NotImplementedError
end
# attributes wrapped in a method to be
# adjusted in sub-class if needed
def prepare_attributes
attributes
end
private
attr_reader :klass, :attributes, :lru_cache, :cache_key
def find_with_cache
return yield unless lru_cache && cache_key
lru_cache[cache_key] ||= yield
end
def cache_from_request_store
Gitlab::SafeRequestStore[:lru_cache] ||= LruRedux::Cache.new(LRU_CACHE_SIZE)
end
def find_object
klass.where(where_clause).first
end
def where_clause
where_clauses.reduce(:and)
end
def table
@table ||= klass.arel_table
end
# Returns Arel clause:
# `"{table_name}"."{attrs.keys[0]}" = '{attrs.values[0]} AND {table_name}"."{attrs.keys[1]}" = '{attrs.values[1]}"`
# from the given Hash of attributes.
def attrs_to_arel(attrs)
attrs.map do |key, value|
table[key].eq(value)
end.reduce(:and)
end
# Returns Arel clause `"{table_name}"."title" = '{attributes['title']}'`
# if attributes has 'title key, otherwise `nil`.
def where_clause_for_title
attrs_to_arel(attributes.slice('title'))
end
# Returns Arel clause `"{table_name}"."description" = '{attributes['description']}'`
# if attributes has 'description key, otherwise `nil`.
def where_clause_for_description
attrs_to_arel(attributes.slice('description'))
end
# Returns Arel clause `"{table_name}"."created_at" = '{attributes['created_at']}'`
# if attributes has 'created_at key, otherwise `nil`.
def where_clause_for_created_at
attrs_to_arel(attributes.slice('created_at'))
end
end
end
end
...@@ -11,61 +11,29 @@ module Gitlab ...@@ -11,61 +11,29 @@ module Gitlab
# finds or initializes a label with the given attributes. # finds or initializes a label with the given attributes.
# #
# It also adds some logic around Group Labels/Milestones for edge cases. # It also adds some logic around Group Labels/Milestones for edge cases.
class GroupProjectObjectBuilder class GroupProjectObjectBuilder < BaseObjectBuilder
# Cache keeps 1000 entries at most, 1000 is chosen based on:
# - one cache entry uses around 0.5K memory, 1000 items uses around 500K.
# (leave some buffer it should be less than 1M). It is afforable cost for project import.
# - for projects in Gitlab.com, it seems 1000 entries for labels/milestones is enough.
# For example, gitlab has ~970 labels and 26 milestones.
LRU_CACHE_SIZE = 1000
def self.build(*args) def self.build(*args)
Project.transaction do Project.transaction do
new(*args).find super
end end
end end
def initialize(klass, attributes) def initialize(klass, attributes)
@klass = klass < Label ? Label : klass super
@attributes = attributes
@group = @attributes['group'] @group = @attributes['group']
@project = @attributes['project'] @project = @attributes['project']
if Gitlab::SafeRequestStore.active?
@lru_cache = cache_from_request_store
@cache_key = [klass, attributes]
end
end end
def find def find
return if epic? && group.nil? return if epic? && group.nil?
find_with_cache do super
find_object || klass.create(project_attributes)
end
end end
private private
attr_reader :klass, :attributes, :group, :project, :lru_cache, :cache_key attr_reader :group, :project
def find_with_cache
return yield unless lru_cache && cache_key
lru_cache[cache_key] ||= yield
end
def cache_from_request_store
Gitlab::SafeRequestStore[:lru_cache] ||= LruRedux::Cache.new(LRU_CACHE_SIZE)
end
def find_object
klass.where(where_clause).first
end
def where_clause
where_clauses.reduce(:and)
end
def where_clauses def where_clauses
[ [
...@@ -86,26 +54,12 @@ module Gitlab ...@@ -86,26 +54,12 @@ module Gitlab
end.reduce(:or) end.reduce(:or)
end end
# Returns Arel clause `"{table_name}"."title" = '{attributes['title']}'` # Returns Arel clause for a particular model or `nil`.
# if attributes has 'title key, otherwise `nil`. def where_clause_for_klass
def where_clause_for_title attrs_to_arel(attributes.slice('iid')) if merge_request?
attrs_to_arel(attributes.slice('title'))
end
# Returns Arel clause:
# `"{table_name}"."{attrs.keys[0]}" = '{attrs.values[0]} AND {table_name}"."{attrs.keys[1]}" = '{attrs.values[1]}"`
# from the given Hash of attributes.
def attrs_to_arel(attrs)
attrs.map do |key, value|
table[key].eq(value)
end.reduce(:and)
end
def table
@table ||= klass.arel_table
end end
def project_attributes def prepare_attributes
attributes.except('group').tap do |atts| attributes.except('group').tap do |atts|
if label? if label?
atts['type'] = 'ProjectLabel' # Always create project labels atts['type'] = 'ProjectLabel' # Always create project labels
...@@ -154,13 +108,6 @@ module Gitlab ...@@ -154,13 +108,6 @@ module Gitlab
milestone.ensure_project_iid! milestone.ensure_project_iid!
milestone.save! milestone.save!
end end
protected
# Returns Arel clause for a particular model or `nil`.
def where_clause_for_klass
return attrs_to_arel(attributes.slice('iid')) if merge_request?
end
end end
end end
end end
......
# frozen_string_literal: true
module Gitlab
module ImportExport
class ProjectRelationFactory < BaseRelationFactory
prepend_if_ee('::EE::Gitlab::ImportExport::ProjectRelationFactory') # rubocop: disable Cop/InjectEnterpriseEditionModule
OVERRIDES = { snippets: :project_snippets,
ci_pipelines: 'Ci::Pipeline',
pipelines: 'Ci::Pipeline',
stages: 'Ci::Stage',
statuses: 'commit_status',
triggers: 'Ci::Trigger',
pipeline_schedules: 'Ci::PipelineSchedule',
builds: 'Ci::Build',
runners: 'Ci::Runner',
hooks: 'ProjectHook',
merge_access_levels: 'ProtectedBranch::MergeAccessLevel',
push_access_levels: 'ProtectedBranch::PushAccessLevel',
create_access_levels: 'ProtectedTag::CreateAccessLevel',
labels: :project_labels,
priorities: :label_priorities,
auto_devops: :project_auto_devops,
label: :project_label,
custom_attributes: 'ProjectCustomAttribute',
project_badges: 'Badge',
metrics: 'MergeRequest::Metrics',
ci_cd_settings: 'ProjectCiCdSetting',
error_tracking_setting: 'ErrorTracking::ProjectErrorTrackingSetting',
links: 'Releases::Link',
metrics_setting: 'ProjectMetricsSetting' }.freeze
BUILD_MODELS = %i[Ci::Build commit_status].freeze
GROUP_REFERENCES = %w[group_id].freeze
PROJECT_REFERENCES = %w[project_id source_project_id target_project_id].freeze
EXISTING_OBJECT_RELATIONS = %i[
milestone
milestones
label
labels
project_label
project_labels
group_label
group_labels
project_feature
merge_request
epic
ProjectCiCdSetting
container_expiration_policy
].freeze
def create
@object = super
# We preload the project, user, and group to re-use objects
@object = preload_keys(@object, PROJECT_REFERENCES, @importable)
@object = preload_keys(@object, GROUP_REFERENCES, @importable.group)
@object = preload_keys(@object, USER_REFERENCES, @user)
end
private
def invalid_relation?
# Do not create relation if it is:
# - An unknown service
# - A legacy trigger
unknown_service? ||
(!Feature.enabled?(:use_legacy_pipeline_triggers, @importable) && legacy_trigger?)
end
def setup_models
case @relation_name
when :merge_request_diff_files then setup_diff
when :notes then setup_note
when :'Ci::Pipeline' then setup_pipeline
when *BUILD_MODELS then setup_build
end
update_project_references
update_group_references
end
def generate_imported_object
if @relation_name == :merge_requests
MergeRequestParser.new(@importable, @relation_hash.delete('diff_head_sha'), super, @relation_hash).parse!
else
super
end
end
def update_project_references
# If source and target are the same, populate them with the new project ID.
if @relation_hash['source_project_id']
@relation_hash['source_project_id'] = same_source_and_target? ? @relation_hash['project_id'] : MergeRequestParser::FORKED_PROJECT_ID
end
@relation_hash['target_project_id'] = @relation_hash['project_id'] if @relation_hash['target_project_id']
end
def same_source_and_target?
@relation_hash['target_project_id'] && @relation_hash['target_project_id'] == @relation_hash['source_project_id']
end
def update_group_references
return unless existing_object?
return unless @relation_hash['group_id']
@relation_hash['group_id'] = @importable.namespace_id
end
# This code is a workaround for broken project exports that don't
# export merge requests with CI pipelines (i.e. exports that were
# generated from
# https://gitlab.com/gitlab-org/gitlab/merge_requests/17844).
# This method can be removed in GitLab 12.6.
def update_merge_request_references
# If a merge request was properly created, we don't need to fix
# up this export.
return if @relation_hash['merge_request']
merge_request_id = @relation_hash['merge_request_id']
return unless merge_request_id
new_merge_request_id = @merge_requests_mapping[merge_request_id]
return unless new_merge_request_id
@relation_hash['merge_request_id'] = new_merge_request_id
parsed_relation_hash['merge_request_id'] = new_merge_request_id
end
def setup_build
@relation_hash.delete('trace') # old export files have trace
@relation_hash.delete('token')
@relation_hash.delete('commands')
@relation_hash.delete('artifacts_file_store')
@relation_hash.delete('artifacts_metadata_store')
@relation_hash.delete('artifacts_size')
end
def setup_diff
@relation_hash['diff'] = @relation_hash.delete('utf8_diff')
end
def setup_pipeline
update_merge_request_references
@relation_hash.fetch('stages', []).each do |stage|
stage.statuses.each do |status|
status.pipeline = imported_object
end
end
end
def unknown_service?
@relation_name == :services && parsed_relation_hash['type'] &&
!Object.const_defined?(parsed_relation_hash['type'])
end
def legacy_trigger?
@relation_name == :'Ci::Trigger' && @relation_hash['owner_id'].nil?
end
def preload_keys(object, references, value)
return object unless value
references.each do |key|
attribute = "#{key.delete_suffix('_id')}=".to_sym
next unless object.respond_to?(key) && object.respond_to?(attribute)
if object.read_attribute(key) == value&.id
object.public_send(attribute, value) # rubocop:disable GitlabSecurity/PublicSend
end
end
object
end
end
end
end
...@@ -48,6 +48,7 @@ module Gitlab ...@@ -48,6 +48,7 @@ module Gitlab
shared: @shared, shared: @shared,
importable: @project, importable: @project,
tree_hash: @tree_hash, tree_hash: @tree_hash,
object_builder: object_builder,
members_mapper: members_mapper, members_mapper: members_mapper,
relation_factory: relation_factory, relation_factory: relation_factory,
reader: reader reader: reader
...@@ -60,8 +61,12 @@ module Gitlab ...@@ -60,8 +61,12 @@ module Gitlab
importable: @project) importable: @project)
end end
def object_builder
Gitlab::ImportExport::GroupProjectObjectBuilder
end
def relation_factory def relation_factory
Gitlab::ImportExport::RelationFactory Gitlab::ImportExport::ProjectRelationFactory
end end
def reader def reader
......
...@@ -11,12 +11,13 @@ module Gitlab ...@@ -11,12 +11,13 @@ module Gitlab
attr_reader :importable attr_reader :importable
attr_reader :tree_hash attr_reader :tree_hash
def initialize(user:, shared:, importable:, tree_hash:, members_mapper:, relation_factory:, reader:) def initialize(user:, shared:, importable:, tree_hash:, members_mapper:, object_builder:, relation_factory:, reader:)
@user = user @user = user
@shared = shared @shared = shared
@importable = importable @importable = importable
@tree_hash = tree_hash @tree_hash = tree_hash
@members_mapper = members_mapper @members_mapper = members_mapper
@object_builder = object_builder
@relation_factory = relation_factory @relation_factory = relation_factory
@reader = reader @reader = reader
end end
...@@ -223,13 +224,14 @@ module Gitlab ...@@ -223,13 +224,14 @@ module Gitlab
base_params = { base_params = {
relation_sym: relation_key.to_sym, relation_sym: relation_key.to_sym,
relation_hash: data_hash, relation_hash: data_hash,
importable: @importable,
members_mapper: @members_mapper, members_mapper: @members_mapper,
object_builder: @object_builder,
user: @user, user: @user,
excluded_keys: excluded_keys_for_relation(relation_key) excluded_keys: excluded_keys_for_relation(relation_key)
} }
base_params[:merge_requests_mapping] = merge_requests_mapping if importable_class == Project base_params[:merge_requests_mapping] = merge_requests_mapping if importable_class == Project
base_params[importable_class_sym] = @importable
base_params base_params
end end
end end
......
...@@ -9235,6 +9235,9 @@ msgstr "" ...@@ -9235,6 +9235,9 @@ msgstr ""
msgid "Group ID: %{group_id}" msgid "Group ID: %{group_id}"
msgstr "" msgstr ""
msgid "Group Owner must have signed in with SAML before enabling Group Managed Accounts"
msgstr ""
msgid "Group Runners" msgid "Group Runners"
msgstr "" msgstr ""
...@@ -14360,6 +14363,9 @@ msgstr "" ...@@ -14360,6 +14363,9 @@ msgstr ""
msgid "ProjectSettings|All discussions must be resolved" msgid "ProjectSettings|All discussions must be resolved"
msgstr "" msgstr ""
msgid "ProjectSettings|Allow users to make copies of your repository to a new project"
msgstr ""
msgid "ProjectSettings|Allow users to request access" msgid "ProjectSettings|Allow users to request access"
msgstr "" msgstr ""
...@@ -14420,6 +14426,9 @@ msgstr "" ...@@ -14420,6 +14426,9 @@ msgstr ""
msgid "ProjectSettings|Fast-forward merges only" msgid "ProjectSettings|Fast-forward merges only"
msgstr "" msgstr ""
msgid "ProjectSettings|Forks"
msgstr ""
msgid "ProjectSettings|Git Large File Storage" msgid "ProjectSettings|Git Large File Storage"
msgstr "" msgstr ""
......
...@@ -15,6 +15,10 @@ module QA ...@@ -15,6 +15,10 @@ module QA
CAPYBARA_MAX_WAIT_TIME = 10 CAPYBARA_MAX_WAIT_TIME = 10
class << self
attr_accessor :rspec_configured, :capybara_configured
end
def initialize def initialize
self.class.configure! self.class.configure!
end end
...@@ -45,11 +49,40 @@ module QA ...@@ -45,11 +49,40 @@ module QA
end end
def self.configure! def self.configure!
configure_rspec!
configure_capybara!
end
def self.configure_rspec!
# We don't want to enter this infinite loop:
# Runtime::Release.perform_before_hooks -> `QA::Runtime::Browser.visit` -> configure! -> configure_rspec! -> Runtime::Release.perform_before_hooks
# So we make sure this method is called only once.
return if self.rspec_configured
browser = self
RSpec.configure do |config| RSpec.configure do |config|
config.define_derived_metadata(file_path: %r{/qa/specs/features/}) do |metadata| config.define_derived_metadata(file_path: %r{/qa/specs/features/}) do |metadata|
metadata[:type] = :feature metadata[:type] = :feature
end end
config.before do
unless browser.rspec_configured
browser.rspec_configured = true
##
# Perform before hooks, which are different for CE and EE
#
Runtime::Release.perform_before_hooks
end
end
end end
end
def self.configure_capybara!
return if self.capybara_configured
self.capybara_configured = true
Capybara.server_port = 9887 + ENV['TEST_ENV_NUMBER'].to_i Capybara.server_port = 9887 + ENV['TEST_ENV_NUMBER'].to_i
......
...@@ -14,11 +14,9 @@ module QA ...@@ -14,11 +14,9 @@ module QA
attr_writer :logger attr_writer :logger
def logger def logger
return @logger if @logger @logger ||= ::Logger.new(Runtime::Env.log_destination).tap do |logger|
logger.level = Runtime::Env.debug? ? ::Logger::DEBUG : ::Logger::ERROR
@logger = ::Logger.new Runtime::Env.log_destination end
@logger.level = Runtime::Env.debug? ? ::Logger::DEBUG : ::Logger::ERROR
@logger
end end
end end
end end
......
...@@ -23,11 +23,6 @@ module QA ...@@ -23,11 +23,6 @@ module QA
def perform(options, *args) def perform(options, *args)
extract_address(:gitlab_address, options, args) extract_address(:gitlab_address, options, args)
##
# Perform before hooks, which are different for CE and EE
#
Runtime::Release.perform_before_hooks
Runtime::Feature.enable(options[:enable_feature]) if options.key?(:enable_feature) Runtime::Feature.enable(options[:enable_feature]) if options.key?(:enable_feature)
Specs::Runner.perform do |specs| Specs::Runner.perform do |specs|
......
...@@ -20,11 +20,6 @@ module QA ...@@ -20,11 +20,6 @@ module QA
def self.do_perform(address, *rspec_options) def self.do_perform(address, *rspec_options)
Runtime::Scenario.define(:gitlab_address, address) Runtime::Scenario.define(:gitlab_address, address)
##
# Perform before hooks, which are different for CE and EE
#
Runtime::Release.perform_before_hooks
Specs::Runner.perform do |specs| Specs::Runner.perform do |specs|
specs.tty = true specs.tty = true
specs.options = rspec_options if rspec_options.any? specs.options = rspec_options if rspec_options.any?
......
...@@ -16,7 +16,7 @@ module QA ...@@ -16,7 +16,7 @@ module QA
super super
end end
def wait_until(max_duration: 60, sleep_interval: 0.1, reload: true) def wait_until(max_duration: 60, sleep_interval: 0.1, reload: true, raise_on_failure: false)
log("next wait uses reload: #{reload}") log("next wait uses reload: #{reload}")
# Logging of wait start/end/duration is handled by QA::Support::Waiter # Logging of wait start/end/duration is handled by QA::Support::Waiter
......
...@@ -62,7 +62,7 @@ describe QA::Page::Base do ...@@ -62,7 +62,7 @@ describe QA::Page::Base do
end end
end end
describe '#wait' do describe '#wait_until' do
subject { Class.new(described_class).new } subject { Class.new(described_class).new }
context 'when the condition is true' do context 'when the condition is true' do
......
...@@ -12,9 +12,9 @@ QA::Runtime::Browser.configure! ...@@ -12,9 +12,9 @@ QA::Runtime::Browser.configure!
QA::Runtime::Scenario.from_env(QA::Runtime::Env.runtime_scenario_attributes) if QA::Runtime::Env.runtime_scenario_attributes QA::Runtime::Scenario.from_env(QA::Runtime::Env.runtime_scenario_attributes) if QA::Runtime::Env.runtime_scenario_attributes
%w[helpers shared_examples].each do |d| Dir[::File.join(__dir__, "support/helpers/*.rb")].each { |f| require f }
Dir[::File.join(__dir__, d, '**', '*.rb')].each { |f| require f } Dir[::File.join(__dir__, "support/shared_contexts/*.rb")].each { |f| require f }
end Dir[::File.join(__dir__, "support/shared_examples/*.rb")].each { |f| require f }
RSpec.configure do |config| RSpec.configure do |config|
QA::Specs::Helpers::Quarantine.configure_rspec QA::Specs::Helpers::Quarantine.configure_rspec
......
...@@ -31,12 +31,6 @@ shared_examples 'a QA scenario class' do ...@@ -31,12 +31,6 @@ shared_examples 'a QA scenario class' do
expect(attributes).to have_received(:define).with(:gitlab_address, 'http://gitlab_address').at_least(:once) expect(attributes).to have_received(:define).with(:gitlab_address, 'http://gitlab_address').at_least(:once)
end end
it 'performs before hooks' do
subject.perform(args)
expect(release).to have_received(:perform_before_hooks)
end
it 'sets tags on runner' do it 'sets tags on runner' do
subject.perform(args) subject.perform(args)
......
...@@ -12,6 +12,21 @@ describe Projects::ForksController do ...@@ -12,6 +12,21 @@ describe Projects::ForksController do
group.add_owner(user) group.add_owner(user)
end end
shared_examples 'forking disabled' do
let(:project) { create(:project, :private, :repository, :forking_disabled) }
before do
project.add_developer(user)
sign_in(user)
end
it 'returns with 404' do
subject
expect(response).to have_gitlab_http_status(404)
end
end
describe 'GET index' do describe 'GET index' do
def get_forks(search: nil) def get_forks(search: nil)
get :index, get :index,
...@@ -138,7 +153,7 @@ describe Projects::ForksController do ...@@ -138,7 +153,7 @@ describe Projects::ForksController do
end end
describe 'GET new' do describe 'GET new' do
def get_new subject do
get :new, get :new,
params: { params: {
namespace_id: project.namespace, namespace_id: project.namespace,
...@@ -150,7 +165,7 @@ describe Projects::ForksController do ...@@ -150,7 +165,7 @@ describe Projects::ForksController do
it 'responds with status 200' do it 'responds with status 200' do
sign_in(user) sign_in(user)
get_new subject
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
end end
...@@ -160,21 +175,26 @@ describe Projects::ForksController do ...@@ -160,21 +175,26 @@ describe Projects::ForksController do
it 'redirects to the sign-in page' do it 'redirects to the sign-in page' do
sign_out(user) sign_out(user)
get_new subject
expect(response).to redirect_to(new_user_session_path) expect(response).to redirect_to(new_user_session_path)
end end
end end
it_behaves_like 'forking disabled'
end end
describe 'POST create' do describe 'POST create' do
def post_create(params = {}) let(:params) do
post :create, {
params: {
namespace_id: project.namespace, namespace_id: project.namespace,
project_id: project, project_id: project,
namespace_key: user.namespace.id namespace_key: user.namespace.id
}.merge(params) }
end
subject do
post :create, params: params
end end
context 'when user is signed in' do context 'when user is signed in' do
...@@ -183,29 +203,47 @@ describe Projects::ForksController do ...@@ -183,29 +203,47 @@ describe Projects::ForksController do
end end
it 'responds with status 302' do it 'responds with status 302' do
post_create subject
expect(response).to have_gitlab_http_status(302) expect(response).to have_gitlab_http_status(302)
expect(response).to redirect_to(namespace_project_import_path(user.namespace, project)) expect(response).to redirect_to(namespace_project_import_path(user.namespace, project))
end end
context 'continue params' do
let(:params) do
{
namespace_id: project.namespace,
project_id: project,
namespace_key: user.namespace.id,
continue: continue_params
}
end
let(:continue_params) do
{
to: '/-/ide/project/path',
notice: 'message'
}
end
it 'passes continue params to the redirect' do it 'passes continue params to the redirect' do
continue_params = { to: '/-/ide/project/path', notice: 'message' } subject
post_create continue: continue_params
expect(response).to have_gitlab_http_status(302) expect(response).to have_gitlab_http_status(302)
expect(response).to redirect_to(namespace_project_import_path(user.namespace, project, continue: continue_params)) expect(response).to redirect_to(namespace_project_import_path(user.namespace, project, continue: continue_params))
end end
end end
end
context 'when user is not signed in' do context 'when user is not signed in' do
it 'redirects to the sign-in page' do it 'redirects to the sign-in page' do
sign_out(user) sign_out(user)
post_create subject
expect(response).to redirect_to(new_user_session_path) expect(response).to redirect_to(new_user_session_path)
end end
end end
it_behaves_like 'forking disabled'
end end
end end
...@@ -25,6 +25,7 @@ FactoryBot.define do ...@@ -25,6 +25,7 @@ FactoryBot.define do
builds_access_level { ProjectFeature::ENABLED } builds_access_level { ProjectFeature::ENABLED }
snippets_access_level { ProjectFeature::ENABLED } snippets_access_level { ProjectFeature::ENABLED }
issues_access_level { ProjectFeature::ENABLED } issues_access_level { ProjectFeature::ENABLED }
forking_access_level { ProjectFeature::ENABLED }
merge_requests_access_level { ProjectFeature::ENABLED } merge_requests_access_level { ProjectFeature::ENABLED }
repository_access_level { ProjectFeature::ENABLED } repository_access_level { ProjectFeature::ENABLED }
pages_access_level do pages_access_level do
...@@ -48,6 +49,7 @@ FactoryBot.define do ...@@ -48,6 +49,7 @@ FactoryBot.define do
builds_access_level: builds_access_level, builds_access_level: builds_access_level,
snippets_access_level: evaluator.snippets_access_level, snippets_access_level: evaluator.snippets_access_level,
issues_access_level: evaluator.issues_access_level, issues_access_level: evaluator.issues_access_level,
forking_access_level: evaluator.forking_access_level,
merge_requests_access_level: merge_requests_access_level, merge_requests_access_level: merge_requests_access_level,
repository_access_level: evaluator.repository_access_level repository_access_level: evaluator.repository_access_level
} }
...@@ -264,6 +266,9 @@ FactoryBot.define do ...@@ -264,6 +266,9 @@ FactoryBot.define do
trait(:issues_disabled) { issues_access_level { ProjectFeature::DISABLED } } trait(:issues_disabled) { issues_access_level { ProjectFeature::DISABLED } }
trait(:issues_enabled) { issues_access_level { ProjectFeature::ENABLED } } trait(:issues_enabled) { issues_access_level { ProjectFeature::ENABLED } }
trait(:issues_private) { issues_access_level { ProjectFeature::PRIVATE } } trait(:issues_private) { issues_access_level { ProjectFeature::PRIVATE } }
trait(:forking_disabled) { forking_access_level { ProjectFeature::DISABLED } }
trait(:forking_enabled) { forking_access_level { ProjectFeature::ENABLED } }
trait(:forking_private) { forking_access_level { ProjectFeature::PRIVATE } }
trait(:merge_requests_enabled) { merge_requests_access_level { ProjectFeature::ENABLED } } trait(:merge_requests_enabled) { merge_requests_access_level { ProjectFeature::ENABLED } }
trait(:merge_requests_disabled) { merge_requests_access_level { ProjectFeature::DISABLED } } trait(:merge_requests_disabled) { merge_requests_access_level { ProjectFeature::DISABLED } }
trait(:merge_requests_private) { merge_requests_access_level { ProjectFeature::PRIVATE } } trait(:merge_requests_private) { merge_requests_access_level { ProjectFeature::PRIVATE } }
......
...@@ -186,7 +186,7 @@ describe 'Edit Project Settings' do ...@@ -186,7 +186,7 @@ describe 'Edit Project Settings' do
click_button "Save changes" click_button "Save changes"
end end
expect(find(".sharing-permissions")).to have_selector(".project-feature-toggle.is-disabled", count: 2) expect(find(".sharing-permissions")).to have_selector(".project-feature-toggle.is-disabled", count: 3)
end end
it "shows empty features project homepage" do it "shows empty features project homepage" do
......
...@@ -27,6 +27,89 @@ describe 'Project fork' do ...@@ -27,6 +27,89 @@ describe 'Project fork' do
expect(page).to have_css('a.disabled', text: 'Fork') expect(page).to have_css('a.disabled', text: 'Fork')
end end
context 'forking enabled / disabled in project settings' do
before do
project.project_feature.update_attribute(
:forking_access_level, forking_access_level)
end
context 'forking is enabled' do
let(:forking_access_level) { ProjectFeature::ENABLED }
it 'enables fork button' do
visit project_path(project)
expect(page).to have_css('a', text: 'Fork')
expect(page).not_to have_css('a.disabled', text: 'Fork')
end
it 'renders new project fork page' do
visit new_project_fork_path(project)
expect(page.status_code).to eq(200)
expect(page).to have_text(' Select a namespace to fork the project ')
end
end
context 'forking is disabled' do
let(:forking_access_level) { ProjectFeature::DISABLED }
it 'does not render fork button' do
visit project_path(project)
expect(page).not_to have_css('a', text: 'Fork')
end
it 'does not render new project fork page' do
visit new_project_fork_path(project)
expect(page.status_code).to eq(404)
end
end
context 'forking is private' do
let(:forking_access_level) { ProjectFeature::PRIVATE }
before do
project.update(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
end
context 'user is not a team member' do
it 'does not render fork button' do
visit project_path(project)
expect(page).not_to have_css('a', text: 'Fork')
end
it 'does not render new project fork page' do
visit new_project_fork_path(project)
expect(page.status_code).to eq(404)
end
end
context 'user is a team member' do
before do
project.add_developer(user)
end
it 'enables fork button' do
visit project_path(project)
expect(page).to have_css('a', text: 'Fork')
expect(page).not_to have_css('a.disabled', text: 'Fork')
end
it 'renders new project fork page' do
visit new_project_fork_path(project)
expect(page.status_code).to eq(200)
expect(page).to have_text(' Select a namespace to fork the project ')
end
end
end
end
it 'forks the project', :sidekiq_might_not_need_inline do it 'forks the project', :sidekiq_might_not_need_inline do
visit project_path(project) visit project_path(project)
......
...@@ -34,6 +34,26 @@ describe 'Projects settings' do ...@@ -34,6 +34,26 @@ describe 'Projects settings' do
expect_toggle_state(:expanded) expect_toggle_state(:expanded)
end end
context 'forking enabled', :js do
it 'toggles forking enabled / disabled' do
visit edit_project_path(project)
forking_enabled_input = find('input[name="project[project_feature_attributes][forking_access_level]"]', visible: :hidden)
forking_enabled_button = find('input[name="project[project_feature_attributes][forking_access_level]"] + label > button')
expect(forking_enabled_input.value).to eq('20')
# disable by clicking toggle
forking_enabled_button.click
page.within('.sharing-permissions') do
find('input[value="Save changes"]').click
end
wait_for_requests
expect(forking_enabled_input.value).to eq('0')
end
end
def expect_toggle_state(state) def expect_toggle_state(state)
is_collapsed = state == :collapsed is_collapsed = state == :collapsed
......
...@@ -15,7 +15,6 @@ describe('Assignee component', () => { ...@@ -15,7 +15,6 @@ describe('Assignee component', () => {
const createWrapper = (propsData = getDefaultProps()) => { const createWrapper = (propsData = getDefaultProps()) => {
wrapper = mount(Assignee, { wrapper = mount(Assignee, {
propsData, propsData,
attachToDocument: true,
}); });
}; };
......
...@@ -23,7 +23,6 @@ describe('AssigneeAvatarLink component', () => { ...@@ -23,7 +23,6 @@ describe('AssigneeAvatarLink component', () => {
}; };
wrapper = shallowMount(AssigneeAvatarLink, { wrapper = shallowMount(AssigneeAvatarLink, {
attachToDocument: true,
propsData, propsData,
}); });
} }
......
...@@ -16,7 +16,6 @@ describe('CollapsedAssigneeList component', () => { ...@@ -16,7 +16,6 @@ describe('CollapsedAssigneeList component', () => {
}; };
wrapper = shallowMount(CollapsedAssigneeList, { wrapper = shallowMount(CollapsedAssigneeList, {
attachToDocument: true,
propsData, propsData,
}); });
} }
......
...@@ -18,7 +18,6 @@ describe('UncollapsedAssigneeList component', () => { ...@@ -18,7 +18,6 @@ describe('UncollapsedAssigneeList component', () => {
}; };
wrapper = mount(UncollapsedAssigneeList, { wrapper = mount(UncollapsedAssigneeList, {
attachToDocument: true,
propsData, propsData,
}); });
} }
......
...@@ -8,4 +8,10 @@ describe GitlabSchema.types['Group'] do ...@@ -8,4 +8,10 @@ describe GitlabSchema.types['Group'] do
it { expect(described_class.graphql_name).to eq('Group') } it { expect(described_class.graphql_name).to eq('Group') }
it { expect(described_class).to require_graphql_authorizations(:read_group) } it { expect(described_class).to require_graphql_authorizations(:read_group) }
it 'has the expected fields' do
expected_fields = %w[web_url avatar_url mentions_disabled parent]
is_expected.to include_graphql_fields(*expected_fields)
end
end end
...@@ -1331,9 +1331,9 @@ module Gitlab ...@@ -1331,9 +1331,9 @@ module Gitlab
stub_feature_flags(ci_release_generation: false) stub_feature_flags(ci_release_generation: false)
end end
it "returns release info" do it 'raises error' do
expect(processor.stage_builds_attributes('release').first[:options].include?(config[:release])) expect { processor }.to raise_error(
.to be false 'jobs:release config release features are not enabled: release')
end end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::ImportExport::BaseObjectBuilder do
let(:project) do
create(:project, :repository,
:builds_disabled,
:issues_disabled,
name: 'project',
path: 'project')
end
let(:klass) { Milestone }
let(:attributes) { { 'title' => 'Test BaseObjectBuilder Milestone', 'project' => project } }
subject { described_class.build(klass, attributes) }
describe '#build' do
context 'when object exists' do
context 'when where_clauses are implemented' do
before do
allow_next_instance_of(described_class) do |object_builder|
allow(object_builder).to receive(:where_clauses).and_return([klass.arel_table['title'].eq(attributes['title'])])
end
end
let!(:milestone) { create(:milestone, title: attributes['title'], project: project) }
it 'finds existing object instead of creating one' do
expect(subject).to eq(milestone)
end
end
context 'when where_clauses are not implemented' do
it 'raises NotImplementedError' do
expect { subject }.to raise_error(NotImplementedError)
end
end
end
context 'when object does not exist' do
before do
allow_next_instance_of(described_class) do |object_builder|
allow(object_builder).to receive(:find_object).and_return(nil)
end
end
it 'creates new object' do
expect { subject }.to change { Milestone.count }.from(0).to(1)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::ImportExport::BaseRelationFactory do
let(:user) { create(:admin) }
let(:project) { create(:project) }
let(:members_mapper) { double('members_mapper').as_null_object }
let(:relation_sym) { :project_snippets }
let(:merge_requests_mapping) { {} }
let(:relation_hash) { {} }
let(:excluded_keys) { [] }
subject do
described_class.create(relation_sym: relation_sym,
relation_hash: relation_hash,
object_builder: Gitlab::ImportExport::GroupProjectObjectBuilder,
members_mapper: members_mapper,
merge_requests_mapping: merge_requests_mapping,
user: user,
importable: project,
excluded_keys: excluded_keys)
end
describe '#create' do
context 'when relation is invalid' do
before do
expect_next_instance_of(described_class) do |relation_factory|
expect(relation_factory).to receive(:invalid_relation?).and_return(true)
end
end
it 'returns without creating new relations' do
expect(subject).to be_nil
end
end
context 'when #setup_models is not implemented' do
it 'raises NotImplementedError' do
expect { subject }.to raise_error(NotImplementedError)
end
end
context 'when #setup_models is implemented' do
let(:relation_sym) { :notes }
let(:relation_hash) do
{
"id" => 4947,
"note" => "merged",
"noteable_type" => "MergeRequest",
"author_id" => 999,
"created_at" => "2016-11-18T09:29:42.634Z",
"updated_at" => "2016-11-18T09:29:42.634Z",
"project_id" => 1,
"attachment" => {
"url" => nil
},
"noteable_id" => 377,
"system" => true,
"events" => []
}
end
before do
expect_next_instance_of(described_class) do |relation_factory|
expect(relation_factory).to receive(:setup_models).and_return(true)
end
end
it 'creates imported object' do
expect(subject).to be_instance_of(Note)
end
context 'when relation contains user references' do
let(:new_user) { create(:user) }
let(:exported_member) do
{
"id" => 111,
"access_level" => 30,
"source_id" => 1,
"source_type" => "Project",
"user_id" => 3,
"notification_level" => 3,
"created_at" => "2016-11-18T09:29:42.634Z",
"updated_at" => "2016-11-18T09:29:42.634Z",
"user" => {
"id" => 999,
"email" => new_user.email,
"username" => new_user.username
}
}
end
let(:members_mapper) do
Gitlab::ImportExport::MembersMapper.new(
exported_members: [exported_member],
user: user,
importable: project)
end
it 'maps the right author to the imported note' do
expect(subject.author).to eq(new_user)
end
end
context 'when relation contains token attributes' do
let(:relation_sym) { 'ProjectHook' }
let(:relation_hash) { { token: 'secret' } }
it 'removes token attributes' do
expect(subject.token).to be_nil
end
end
context 'when relation contains encrypted attributes' do
let(:relation_sym) { 'Ci::Variable' }
let(:relation_hash) do
create(:ci_variable).as_json
end
it 'removes encrypted attributes' do
expect(subject.value).to be_nil
end
end
end
end
describe '.relation_class' do
context 'when relation name is pluralized' do
let(:relation_name) { 'MergeRequest::Metrics' }
it 'returns constantized class' do
expect(described_class.relation_class(relation_name)).to eq(MergeRequest::Metrics)
end
end
context 'when relation name is singularized' do
let(:relation_name) { 'Badge' }
it 'returns constantized class' do
expect(described_class.relation_class(relation_name)).to eq(Badge)
end
end
end
end
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe Gitlab::ImportExport::RelationFactory do describe Gitlab::ImportExport::ProjectRelationFactory do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:project) { create(:project, :repository, group: group) } let(:project) { create(:project, :repository, group: group) }
let(:members_mapper) { double('members_mapper').as_null_object } let(:members_mapper) { double('members_mapper').as_null_object }
...@@ -12,10 +12,11 @@ describe Gitlab::ImportExport::RelationFactory do ...@@ -12,10 +12,11 @@ describe Gitlab::ImportExport::RelationFactory do
let(:created_object) do let(:created_object) do
described_class.create(relation_sym: relation_sym, described_class.create(relation_sym: relation_sym,
relation_hash: relation_hash, relation_hash: relation_hash,
object_builder: Gitlab::ImportExport::GroupProjectObjectBuilder,
members_mapper: members_mapper, members_mapper: members_mapper,
merge_requests_mapping: merge_requests_mapping, merge_requests_mapping: merge_requests_mapping,
user: user, user: user,
project: project, importable: project,
excluded_keys: excluded_keys) excluded_keys: excluded_keys)
end end
...@@ -97,7 +98,7 @@ describe Gitlab::ImportExport::RelationFactory do ...@@ -97,7 +98,7 @@ describe Gitlab::ImportExport::RelationFactory do
end end
end end
context 'merge_requset object' do context 'merge_request object' do
let(:relation_sym) { :merge_requests } let(:relation_sym) { :merge_requests }
let(:exported_member) do let(:exported_member) do
...@@ -244,11 +245,11 @@ describe Gitlab::ImportExport::RelationFactory do ...@@ -244,11 +245,11 @@ describe Gitlab::ImportExport::RelationFactory do
context 'Project references' do context 'Project references' do
let(:relation_sym) { :project_foo_model } let(:relation_sym) { :project_foo_model }
let(:relation_hash) do let(:relation_hash) do
Gitlab::ImportExport::RelationFactory::PROJECT_REFERENCES.map { |ref| { ref => 99 } }.inject(:merge) Gitlab::ImportExport::ProjectRelationFactory::PROJECT_REFERENCES.map { |ref| { ref => 99 } }.inject(:merge)
end end
class ProjectFooModel < FooModel class ProjectFooModel < FooModel
attr_accessor(*Gitlab::ImportExport::RelationFactory::PROJECT_REFERENCES) attr_accessor(*Gitlab::ImportExport::ProjectRelationFactory::PROJECT_REFERENCES)
end end
before do before do
......
...@@ -27,6 +27,7 @@ describe Gitlab::ImportExport::RelationTreeRestorer do ...@@ -27,6 +27,7 @@ describe Gitlab::ImportExport::RelationTreeRestorer do
shared: shared, shared: shared,
tree_hash: tree_hash, tree_hash: tree_hash,
importable: importable, importable: importable,
object_builder: object_builder,
members_mapper: members_mapper, members_mapper: members_mapper,
relation_factory: relation_factory, relation_factory: relation_factory,
reader: reader reader: reader
...@@ -38,7 +39,8 @@ describe Gitlab::ImportExport::RelationTreeRestorer do ...@@ -38,7 +39,8 @@ describe Gitlab::ImportExport::RelationTreeRestorer do
context 'when restoring a project' do context 'when restoring a project' do
let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/project.json' } let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/project.json' }
let(:importable) { create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') } let(:importable) { create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') }
let(:relation_factory) { Gitlab::ImportExport::RelationFactory } let(:object_builder) { Gitlab::ImportExport::GroupProjectObjectBuilder }
let(:relation_factory) { Gitlab::ImportExport::ProjectRelationFactory }
let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) } let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) }
let(:tree_hash) { importable_hash } let(:tree_hash) { importable_hash }
......
...@@ -545,6 +545,7 @@ ProjectFeature: ...@@ -545,6 +545,7 @@ ProjectFeature:
- id - id
- project_id - project_id
- merge_requests_access_level - merge_requests_access_level
- forking_access_level
- issues_access_level - issues_access_level
- wiki_access_level - wiki_access_level
- snippets_access_level - snippets_access_level
......
...@@ -64,6 +64,22 @@ describe ErrorTracking::ProjectErrorTrackingSetting do ...@@ -64,6 +64,22 @@ describe ErrorTracking::ProjectErrorTrackingSetting do
end end
end end
describe '.extract_sentry_external_url' do
subject { described_class.extract_sentry_external_url(sentry_url) }
describe 'when passing a URL' do
let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' }
it { is_expected.to eq('https://sentrytest.gitlab.com/sentry-org/sentry-project') }
end
describe 'when passing nil' do
let(:sentry_url) { nil }
it { is_expected.to be_nil }
end
end
describe '#sentry_external_url' do describe '#sentry_external_url' do
let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' } let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' }
......
...@@ -358,6 +358,7 @@ describe API::Groups do ...@@ -358,6 +358,7 @@ describe API::Groups do
expect(json_response['two_factor_grace_period']).to eq(group1.two_factor_grace_period) expect(json_response['two_factor_grace_period']).to eq(group1.two_factor_grace_period)
expect(json_response['auto_devops_enabled']).to eq(group1.auto_devops_enabled) expect(json_response['auto_devops_enabled']).to eq(group1.auto_devops_enabled)
expect(json_response['emails_disabled']).to eq(group1.emails_disabled) expect(json_response['emails_disabled']).to eq(group1.emails_disabled)
expect(json_response['mentions_disabled']).to eq(group1.mentions_disabled)
expect(json_response['project_creation_level']).to eq('maintainer') expect(json_response['project_creation_level']).to eq('maintainer')
expect(json_response['subgroup_creation_level']).to eq('maintainer') expect(json_response['subgroup_creation_level']).to eq('maintainer')
expect(json_response['web_url']).to eq(group1.web_url) expect(json_response['web_url']).to eq(group1.web_url)
...@@ -556,6 +557,7 @@ describe API::Groups do ...@@ -556,6 +557,7 @@ describe API::Groups do
expect(json_response['two_factor_grace_period']).to eq(48) expect(json_response['two_factor_grace_period']).to eq(48)
expect(json_response['auto_devops_enabled']).to eq(nil) expect(json_response['auto_devops_enabled']).to eq(nil)
expect(json_response['emails_disabled']).to eq(nil) expect(json_response['emails_disabled']).to eq(nil)
expect(json_response['mentions_disabled']).to eq(nil)
expect(json_response['project_creation_level']).to eq("noone") expect(json_response['project_creation_level']).to eq("noone")
expect(json_response['subgroup_creation_level']).to eq("maintainer") expect(json_response['subgroup_creation_level']).to eq("maintainer")
expect(json_response['request_access_enabled']).to eq(true) expect(json_response['request_access_enabled']).to eq(true)
......
...@@ -2858,6 +2858,20 @@ describe API::Projects do ...@@ -2858,6 +2858,20 @@ describe API::Projects do
expect(json_response['message']).to eq('401 Unauthorized') expect(json_response['message']).to eq('401 Unauthorized')
end end
end end
context 'forking disabled' do
before do
project.project_feature.update_attribute(
:forking_access_level, ProjectFeature::DISABLED)
end
it 'denies project to be forked' do
post api("/projects/#{project.id}/fork", admin)
expect(response).to have_gitlab_http_status(409)
expect(json_response['message']['forked_from_project_id']).to eq(['is forbidden'])
end
end
end end
describe 'POST /projects/:id/housekeeping' do describe 'POST /projects/:id/housekeeping' do
......
...@@ -224,6 +224,19 @@ describe Projects::ForkService do ...@@ -224,6 +224,19 @@ describe Projects::ForkService do
end end
end end
end end
context 'when forking is disabled' do
before do
@from_project.project_feature.update_attribute(
:forking_access_level, ProjectFeature::DISABLED)
end
it 'fails' do
to_project = fork_project(@from_project, @to_user, namespace: @to_user.namespace)
expect(to_project.errors[:forked_from_project_id]).to eq(['is forbidden'])
end
end
end end
describe 'fork to namespace' do describe 'fork to namespace' do
......
...@@ -76,28 +76,14 @@ describe SystemNoteService do ...@@ -76,28 +76,14 @@ describe SystemNoteService do
end end
describe '.change_due_date' do describe '.change_due_date' do
subject { described_class.change_due_date(noteable, project, author, due_date) } let(:due_date) { double }
let(:due_date) { Date.today } it 'calls TimeTrackingService' do
expect_next_instance_of(::SystemNotes::TimeTrackingService) do |service|
it_behaves_like 'a note with overridable created_at' expect(service).to receive(:change_due_date).with(due_date)
it_behaves_like 'a system note' do
let(:action) { 'due_date' }
end
context 'when due date added' do
it 'sets the note text' do
expect(subject.note).to eq "changed due date to #{Date.today.to_s(:long)}"
end
end end
context 'when due date removed' do described_class.change_due_date(noteable, project, author, due_date)
let(:due_date) { nil }
it 'sets the note text' do
expect(subject.note).to eq 'removed due date'
end
end end
end end
...@@ -488,36 +474,12 @@ describe SystemNoteService do ...@@ -488,36 +474,12 @@ describe SystemNoteService do
end end
describe '.change_time_estimate' do describe '.change_time_estimate' do
subject { described_class.change_time_estimate(noteable, project, author) } it 'calls TimeTrackingService' do
expect_next_instance_of(::SystemNotes::TimeTrackingService) do |service|
it_behaves_like 'a system note' do expect(service).to receive(:change_time_estimate)
let(:action) { 'time_tracking' }
end end
context 'with a time estimate' do described_class.change_time_estimate(noteable, project, author)
it 'sets the note text' do
noteable.update_attribute(:time_estimate, 277200)
expect(subject.note).to eq "changed time estimate to 1w 4d 5h"
end
context 'when time_tracking_limit_to_hours setting is true' do
before do
stub_application_setting(time_tracking_limit_to_hours: true)
end
it 'sets the note text' do
noteable.update_attribute(:time_estimate, 277200)
expect(subject.note).to eq "changed time estimate to 77h"
end
end
end
context 'without a time estimate' do
it 'sets the note text' do
expect(subject.note).to eq "removed time estimate"
end
end end
end end
...@@ -548,62 +510,13 @@ describe SystemNoteService do ...@@ -548,62 +510,13 @@ describe SystemNoteService do
end end
describe '.change_time_spent' do describe '.change_time_spent' do
# We need a custom noteable in order to the shared examples to be green. it 'calls TimeTrackingService' do
let(:noteable) do expect_next_instance_of(::SystemNotes::TimeTrackingService) do |service|
mr = create(:merge_request, source_project: project) expect(service).to receive(:change_time_spent)
mr.spend_time(duration: 360000, user_id: author.id)
mr.save!
mr
end end
subject do
described_class.change_time_spent(noteable, project, author) described_class.change_time_spent(noteable, project, author)
end end
it_behaves_like 'a system note' do
let(:action) { 'time_tracking' }
end
context 'when time was added' do
it 'sets the note text' do
spend_time!(277200)
expect(subject.note).to eq "added 1w 4d 5h of time spent"
end
end
context 'when time was subtracted' do
it 'sets the note text' do
spend_time!(-277200)
expect(subject.note).to eq "subtracted 1w 4d 5h of time spent"
end
end
context 'when time was removed' do
it 'sets the note text' do
spend_time!(:reset)
expect(subject.note).to eq "removed time spent"
end
end
context 'when time_tracking_limit_to_hours setting is true' do
before do
stub_application_setting(time_tracking_limit_to_hours: true)
end
it 'sets the note text' do
spend_time!(277200)
expect(subject.note).to eq "added 77h of time spent"
end
end
def spend_time!(seconds)
noteable.spend_time(duration: seconds, user_id: author.id)
noteable.save!
end
end end
describe '.handle_merge_request_wip' do describe '.handle_merge_request_wip' do
......
# frozen_string_literal: true
require 'spec_helper'
describe ::SystemNotes::TimeTrackingService do
let_it_be(:author) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
let(:noteable) { create(:issue, project: project) }
describe '#change_due_date' do
subject { described_class.new(noteable: noteable, project: project, author: author).change_due_date(due_date) }
let(:due_date) { Date.today }
it_behaves_like 'a note with overridable created_at'
it_behaves_like 'a system note' do
let(:action) { 'due_date' }
end
context 'when due date added' do
it 'sets the note text' do
expect(subject.note).to eq "changed due date to #{due_date.to_s(:long)}"
end
end
context 'when due date removed' do
let(:due_date) { nil }
it 'sets the note text' do
expect(subject.note).to eq 'removed due date'
end
end
end
describe '.change_time_estimate' do
subject { described_class.new(noteable: noteable, project: project, author: author).change_time_estimate }
it_behaves_like 'a system note' do
let(:action) { 'time_tracking' }
end
context 'with a time estimate' do
it 'sets the note text' do
noteable.update_attribute(:time_estimate, 277200)
expect(subject.note).to eq "changed time estimate to 1w 4d 5h"
end
context 'when time_tracking_limit_to_hours setting is true' do
before do
stub_application_setting(time_tracking_limit_to_hours: true)
end
it 'sets the note text' do
noteable.update_attribute(:time_estimate, 277200)
expect(subject.note).to eq "changed time estimate to 77h"
end
end
end
context 'without a time estimate' do
it 'sets the note text' do
expect(subject.note).to eq "removed time estimate"
end
end
end
describe '.change_time_spent' do
# We need a custom noteable in order to the shared examples to be green.
let(:noteable) do
mr = create(:merge_request, source_project: project)
mr.spend_time(duration: 360000, user_id: author.id)
mr.save!
mr
end
subject do
described_class.new(noteable: noteable, project: project, author: author).change_time_spent
end
it_behaves_like 'a system note' do
let(:action) { 'time_tracking' }
end
context 'when time was added' do
it 'sets the note text' do
spend_time!(277200)
expect(subject.note).to eq "added 1w 4d 5h of time spent"
end
end
context 'when time was subtracted' do
it 'sets the note text' do
spend_time!(-277200)
expect(subject.note).to eq "subtracted 1w 4d 5h of time spent"
end
end
context 'when time was removed' do
it 'sets the note text' do
spend_time!(:reset)
expect(subject.note).to eq "removed time spent"
end
end
context 'when time_tracking_limit_to_hours setting is true' do
before do
stub_application_setting(time_tracking_limit_to_hours: true)
end
it 'sets the note text' do
spend_time!(277200)
expect(subject.note).to eq "added 77h of time spent"
end
end
def spend_time!(seconds)
noteable.spend_time(duration: seconds, user_id: author.id)
noteable.save!
end
end
end
...@@ -36,8 +36,8 @@ module ConfigurationHelper ...@@ -36,8 +36,8 @@ module ConfigurationHelper
end end
def relation_class_for_name(relation_name) def relation_class_for_name(relation_name)
relation_name = Gitlab::ImportExport::RelationFactory.overrides[relation_name.to_sym] || relation_name relation_name = Gitlab::ImportExport::ProjectRelationFactory.overrides[relation_name.to_sym] || relation_name
Gitlab::ImportExport::RelationFactory.relation_class(relation_name) Gitlab::ImportExport::ProjectRelationFactory.relation_class(relation_name)
end end
def parsed_attributes(relation_name, attributes, config: Gitlab::ImportExport.config_file) def parsed_attributes(relation_name, attributes, config: Gitlab::ImportExport.config_file)
......
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