Commit 619d0b69 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 17ab40ca
......@@ -64,10 +64,13 @@ class Projects::CommitsController < Projects::ApplicationController
render_404 unless @path.empty? || request.format == :atom || @repository.blob_at(@commit.id, @path) || @repository.tree(@commit.id, @path).entries.present?
@limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i
search = params[:search]
author = params[:author]
@commits =
if search.present?
@repository.find_commits_by_message(search, @ref, @path, @limit, @offset)
elsif author.present?
@repository.commits(@ref, author: author, path: @path, limit: @limit, offset: @offset)
else
@repository.commits(@ref, path: @path, limit: @limit, offset: @offset)
end
......
......@@ -7,7 +7,7 @@ module Repositories
before_action :access_check
prepend_before_action :deny_head_requests, only: [:info_refs]
rescue_from Gitlab::GitAccess::UnauthorizedError, with: :render_403_with_exception
rescue_from Gitlab::GitAccess::ForbiddenError, with: :render_403_with_exception
rescue_from Gitlab::GitAccess::NotFoundError, with: :render_404_with_exception
rescue_from Gitlab::GitAccess::ProjectCreationError, with: :render_422_with_exception
rescue_from Gitlab::GitAccess::TimeoutError, with: :render_503_with_exception
......
......@@ -599,7 +599,7 @@ module Ci
# Manually set the notes for a Ci::Pipeline
# There is no ActiveRecord relation between Ci::Pipeline and notes
# as they are related to a commit sha. This method helps importing
# them using the +Gitlab::ImportExport::ProjectRelationFactory+ class.
# them using the +Gitlab::ImportExport::Project::RelationFactory+ class.
def notes=(notes)
notes.each do |note|
note[:id] = nil
......
......@@ -139,6 +139,7 @@ class Repository
repo: raw_repository,
ref: ref,
path: opts[:path],
author: opts[:author],
follow: Array(opts[:path]).length == 1,
limit: opts[:limit],
offset: opts[:offset],
......
......@@ -18,6 +18,8 @@ module Groups
end
save!
ensure
cleanup
end
private
......@@ -28,7 +30,7 @@ module Groups
if savers.all?(&:save)
notify_success
else
cleanup_and_notify_error!
notify_error!
end
end
......@@ -37,21 +39,19 @@ module Groups
end
def tree_exporter
Gitlab::ImportExport::GroupTreeSaver.new(group: @group, current_user: @current_user, shared: @shared, params: @params)
Gitlab::ImportExport::Group::TreeSaver.new(group: @group, current_user: @current_user, shared: @shared, params: @params)
end
def file_saver
Gitlab::ImportExport::Saver.new(exportable: @group, shared: @shared)
end
def cleanup_and_notify_error
FileUtils.rm_rf(shared.export_path)
notify_error
def cleanup
FileUtils.rm_rf(shared.archive_path) if shared&.archive_path
end
def cleanup_and_notify_error!
cleanup_and_notify_error
def notify_error!
notify_error
raise Gitlab::ImportExport::Error.new(shared.errors.to_sentence)
end
......
......@@ -34,7 +34,7 @@ module Groups
end
def restorer
@restorer ||= Gitlab::ImportExport::GroupTreeRestorer.new(user: @current_user,
@restorer ||= Gitlab::ImportExport::Group::TreeRestorer.new(user: @current_user,
shared: @shared,
group: @group,
group_hash: nil)
......
......@@ -4,13 +4,13 @@ module Lfs
class LockFileService < BaseService
def execute
unless can?(current_user, :push_code, project)
raise Gitlab::GitAccess::UnauthorizedError, 'You have no permissions'
raise Gitlab::GitAccess::ForbiddenError, 'You have no permissions'
end
create_lock!
rescue ActiveRecord::RecordNotUnique
error('already locked', 409, current_lock)
rescue Gitlab::GitAccess::UnauthorizedError => ex
rescue Gitlab::GitAccess::ForbiddenError => ex
error(ex.message, 403)
rescue => ex
error(ex.message, 500)
......
......@@ -4,11 +4,11 @@ module Lfs
class UnlockFileService < BaseService
def execute
unless can?(current_user, :push_code, project)
raise Gitlab::GitAccess::UnauthorizedError, _('You have no permissions')
raise Gitlab::GitAccess::ForbiddenError, _('You have no permissions')
end
unlock_file
rescue Gitlab::GitAccess::UnauthorizedError => ex
rescue Gitlab::GitAccess::ForbiddenError => ex
error(ex.message, 403)
rescue ActiveRecord::RecordNotFound
error(_('Lock not found'), 404)
......
......@@ -14,6 +14,8 @@ module Projects
save_all!
execute_after_export_action(after_export_strategy)
ensure
cleanup
end
private
......@@ -24,7 +26,7 @@ module Projects
return unless after_export_strategy
unless after_export_strategy.execute(current_user, project)
cleanup_and_notify_error
notify_error
end
end
......@@ -33,7 +35,7 @@ module Projects
Gitlab::ImportExport::Saver.save(exportable: project, shared: shared)
notify_success
else
cleanup_and_notify_error!
notify_error!
end
end
......@@ -54,7 +56,7 @@ module Projects
end
def project_tree_saver
Gitlab::ImportExport::ProjectTreeSaver.new(project: project, current_user: current_user, shared: shared, params: params)
Gitlab::ImportExport::Project::TreeSaver.new(project: project, current_user: current_user, shared: shared, params: params)
end
def uploads_saver
......@@ -73,16 +75,12 @@ module Projects
Gitlab::ImportExport::LfsSaver.new(project: project, shared: shared)
end
def cleanup_and_notify_error
Rails.logger.error("Import/Export - Project #{project.name} with ID: #{project.id} export error - #{shared.errors.join(', ')}") # rubocop:disable Gitlab/RailsLogger
FileUtils.rm_rf(shared.export_path)
notify_error
def cleanup
FileUtils.rm_rf(shared.archive_path) if shared&.archive_path
end
def cleanup_and_notify_error!
cleanup_and_notify_error
def notify_error!
notify_error
raise Gitlab::ImportExport::Error.new(shared.errors.to_sentence)
end
......@@ -92,6 +90,8 @@ module Projects
end
def notify_error
Rails.logger.error("Import/Export - Project #{project.name} with ID: #{project.id} export error - #{shared.errors.join(', ')}") # rubocop:disable Gitlab/RailsLogger
notification_service.project_not_exported(project, current_user, shared.errors)
end
end
......
---
title: Do not parse undefined severity and confidence from reports
merge_request: 25884
author:
type: other
---
title: Filter commits by author
merge_request: 25597
author:
type: added
---
title: Align git returned error codes
merge_request: 25936
author:
type: changed
---
title: Ensure temp export data is removed if Group/Project export failed
merge_request: 25828
author:
type: fixed
GEMFILE_LOCK_NOT_UPDATED_MESSAGE_SHORT = <<~MSG.freeze
%<gemfile>s was updated but %<gemfile_lock>s wasn't updated.
MSG
GEMFILE_LOCK_NOT_UPDATED_MESSAGE_FULL = <<~MSG.freeze
**#{GEMFILE_LOCK_NOT_UPDATED_MESSAGE_SHORT}**
Usually, when %<gemfile>s is updated, you should run
```
bundle install
```
or
```
bundle update <the-added-or-updated-gem>
```
and commit the %<gemfile_lock>s changes.
MSG
gemfile_modified = git.modified_files.include?("Gemfile")
gemfile_lock_modified = git.modified_files.include?("Gemfile.lock")
if gemfile_modified && !gemfile_lock_modified
gitlab_danger = GitlabDanger.new(helper.gitlab_helper)
format_str = gitlab_danger.ci? ? GEMFILE_LOCK_NOT_UPDATED_MESSAGE_FULL : GEMFILE_LOCK_NOT_UPDATED_MESSAGE_SHORT
message = format(format_str,
gemfile: gitlab_danger.html_link("Gemfile"),
gemfile_lock: gitlab_danger.html_link("Gemfile.lock")
)
warn(message)
end
......@@ -17,3 +17,4 @@ GitLab’s [security features](../security/README.md) may also help you meet rel
|**[Audit logs](audit_events.md)**<br>To maintain the integrity of your code, GitLab Enterprise Edition Premium gives admins the ability to view any modifications made within the GitLab server in an advanced audit log system, so you can control, analyze, and track every change.|Premium+||
|**[Auditor users](auditor_users.md)**<br>Auditor users are users who are given read-only access to all projects, groups, and other resources on the GitLab instance.|Premium+||
|**[Credentials inventory](../user/admin_area/credentials_inventory.md)**<br>With a credentials inventory, GitLab administrators can keep track of the credentials used by all of the users in their GitLab instance. |Ultimate||
|**Separation of Duties using [Protected branches](../user/project/protected_branches.md#protected-branches-approval-by-code-owners-premium) and [custom CI Configuration Paths](../user/project/pipelines/settings.md#custom-ci-configuration-path)**<br> GitLab Silver and Premium users can leverage GitLab's cross-project YAML configuration's to define deployers of code and developers of code. View the [Separation of Duties Deploy Project](https://gitlab.com/guided-explorations/separation-of-duties-deploy/blob/master/README.md) and [Separation of Duties Project](https://gitlab.com/guided-explorations/separation-of-duties/blob/master/README.md) to see how to use this set up to define these roles.|Premium+||
......@@ -119,6 +119,9 @@ upload packages:
}
```
NOTE: **Note:**
Some build tools, like Gradle, must make `HEAD` requests to Amazon S3 to pull a dependency’s metadata. The `gitlab_rails['packages_object_store_proxy_download']` property must be set to `true`. Without this setting, GitLab won't act as a proxy to the Amazon S3 service, and will instead return the signed URL. This will cause a `HTTP 403 Forbidden` response, since Amazon S3 expects a signed URL.
1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure)
for the changes to take effect.
......
......@@ -81,7 +81,7 @@ The last option is to import a project using a Rails console:
sudo -u git -H bundle exec rails console RAILS_ENV=production
```
1. Create a project and run `ProjectTreeRestorer`:
1. Create a project and run `Project::TreeRestorer`:
```ruby
shared_class = Struct.new(:export_path) do
......@@ -98,7 +98,7 @@ The last option is to import a project using a Rails console:
begin
#Enable Request store
RequestStore.begin!
Gitlab::ImportExport::ProjectTreeRestorer.new(user: user, shared: shared, project: project).restore
Gitlab::ImportExport::Project::TreeRestorer.new(user: user, shared: shared, project: project).restore
ensure
RequestStore.end!
RequestStore.clear!
......@@ -128,11 +128,11 @@ The last option is to import a project using a Rails console:
For Performance testing, we should:
- Import a quite large project, [`gitlabhq`](https://gitlab.com/gitlab-org/quality/performance-data#gitlab-performance-test-framework-data) should be a good example.
- Measure the execution time of `ProjectTreeRestorer`.
- Measure the execution time of `Project::TreeRestorer`.
- Count the number of executed SQL queries during the restore.
- Observe the number of GC cycles happening.
You can use this [snippet](https://gitlab.com/gitlab-org/gitlab/snippets/1924954), which will restore the project, and measure the execution time of `ProjectTreeRestorer`, number of SQL queries and number of GC cycles happening.
You can use this [snippet](https://gitlab.com/gitlab-org/gitlab/snippets/1924954), which will restore the project, and measure the execution time of `Project::TreeRestorer`, number of SQL queries and number of GC cycles happening.
You can execute the script from the `gdk/gitlab` directory like this:
......
......@@ -257,7 +257,7 @@ use an A record. If your external endpoint is a hostname, use a CNAME record.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21966) in GitLab 12.7.
A Web Application Firewall (WAF) is able to examine traffic being sent/received
A Web Application Firewall (WAF) examines traffic being sent or received,
and can block malicious traffic before it reaches your application. The benefits
of a WAF are:
......@@ -266,7 +266,7 @@ of a WAF are:
- Access control for your application
- Highly configurable logging and blocking rules
Out of the box, GitLab provides you with a WAF known as [`ModSecurity`](https://www.modsecurity.org/)
Out of the box, GitLab provides you with a WAF known as [`ModSecurity`](https://www.modsecurity.org/).
ModSecurity is a toolkit for real-time web application monitoring, logging,
and access control. With GitLab's offering, the [OWASP's Core Rule Set](https://www.modsecurity.org/CRS/Documentation/),
......@@ -288,9 +288,6 @@ when installing your [Ingress application](#ingress).
If this is your first time using GitLab's WAF, we recommend you follow the
[quick start guide](../../topics/web_application_firewall/quick_start_guide.md).
There is a small performance overhead by enabling ModSecurity. However,
if this is considered significant for your application, you can disable it.
There is a small performance overhead by enabling ModSecurity. If this is
considered significant for your application, you can disable ModSecurity's
rule engine for your deployed application by setting
......@@ -693,7 +690,7 @@ cilium:
```
The `clusterType` variable enables the recommended Helm variables for
a corresponding cluster type, the default value is blank. You can
a corresponding cluster type. The default value is blank. You can
check the recommended variables for each cluster type in the official
documentation:
......@@ -720,13 +717,13 @@ information.
By default, Cilium will drop all non-whitelisted packets upon policy
deployment. The audit mode is scheduled for release in
[Cilium 1.8](https://github.com/cilium/cilium/pull/9970). In the audit
mode non-whitelisted packets will not be dropped, instead audit
notifications will be generated. GitLab provides alternative Docker
mode, non-whitelisted packets will not be dropped, and audit
notifications will be generated instead. GitLab provides alternative Docker
images for Cilium with the audit patch included. You can switch to the
custom build and enable the audit mode by adding the following to
`.gitlab/managed-apps/cilium/values.yaml`:
```yml
```yaml
global:
registry: registry.gitlab.com/gitlab-org/defend/cilium
policyAuditMode: true
......@@ -737,15 +734,15 @@ agent:
```
The Cilium monitor log for traffic is logged out by the
`cilium-monitor` sidecar container. You can check these logs via:
`cilium-monitor` sidecar container. You can check these logs with the following command:
```shell
kubectl -n gitlab-managed-apps logs cilium-XXXX cilium-monitor
```
You can disable the monitor log via `.gitlab/managed-apps/cilium/values.yaml`:
You can disable the monitor log in `.gitlab/managed-apps/cilium/values.yaml`:
```yml
```yaml
agent:
monitor:
enabled: false
......
......@@ -89,7 +89,7 @@ The following items will NOT be exported:
NOTE: **Note:**
For more details on the specific data persisted in a project export, see the
[`import_export.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/import_export/import_export.yml) file.
[`import_export.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/import_export/project/import_export.yml) file.
## Exporting a project and its data
......
......@@ -49,8 +49,8 @@ module API
result = access_checker.check(params[:action], params[:changes])
@project ||= access_checker.project
result
rescue Gitlab::GitAccess::UnauthorizedError => e
return response_with_status(code: 401, success: false, message: e.message)
rescue Gitlab::GitAccess::ForbiddenError => e
return response_with_status(code: 403, success: false, message: e.message)
rescue Gitlab::GitAccess::TimeoutError => e
return response_with_status(code: 503, success: false, message: e.message)
rescue Gitlab::GitAccess::NotFoundError => e
......
......@@ -28,7 +28,7 @@ module Gitlab
logger.log_timed(LOG_MESSAGES[:delete_default_branch_check]) do
if deletion? && branch_name == project.default_branch
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_default_branch]
raise GitAccess::ForbiddenError, ERROR_MESSAGES[:delete_default_branch]
end
end
......@@ -42,7 +42,7 @@ module Gitlab
return unless ProtectedBranch.protected?(project, branch_name) # rubocop:disable Cop/AvoidReturnFromBlocks
if forced_push?
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:force_push_protected_branch]
raise GitAccess::ForbiddenError, ERROR_MESSAGES[:force_push_protected_branch]
end
end
......@@ -62,15 +62,15 @@ module Gitlab
break if user_access.can_push_to_branch?(branch_name)
unless user_access.can_merge_to_branch?(branch_name)
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:create_protected_branch]
raise GitAccess::ForbiddenError, ERROR_MESSAGES[:create_protected_branch]
end
unless safe_commit_for_new_protected_branch?
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:invalid_commit_create_protected_branch]
raise GitAccess::ForbiddenError, ERROR_MESSAGES[:invalid_commit_create_protected_branch]
end
unless updated_from_web?
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_web_create_protected_branch]
raise GitAccess::ForbiddenError, ERROR_MESSAGES[:non_web_create_protected_branch]
end
end
end
......@@ -78,11 +78,11 @@ module Gitlab
def protected_branch_deletion_checks
logger.log_timed(LOG_MESSAGES[:protected_branch_deletion_checks]) do
unless user_access.can_delete_branch?(branch_name)
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_master_delete_protected_branch]
raise GitAccess::ForbiddenError, ERROR_MESSAGES[:non_master_delete_protected_branch]
end
unless updated_from_web?
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_web_delete_protected_branch]
raise GitAccess::ForbiddenError, ERROR_MESSAGES[:non_web_delete_protected_branch]
end
end
end
......@@ -91,11 +91,11 @@ module Gitlab
logger.log_timed(LOG_MESSAGES[:protected_branch_push_checks]) do
if matching_merge_request?
unless user_access.can_merge_to_branch?(branch_name) || user_access.can_push_to_branch?(branch_name)
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:merge_protected_branch]
raise GitAccess::ForbiddenError, ERROR_MESSAGES[:merge_protected_branch]
end
else
unless user_access.can_push_to_branch?(branch_name)
raise GitAccess::UnauthorizedError, push_to_protected_branch_rejected_message
raise GitAccess::ForbiddenError, push_to_protected_branch_rejected_message
end
end
end
......
......@@ -46,7 +46,7 @@ module Gitlab
def validate_diff(diff)
validations_for_diff.each do |validation|
if error = validation.call(diff)
raise ::Gitlab::GitAccess::UnauthorizedError, error
raise ::Gitlab::GitAccess::ForbiddenError, error
end
end
end
......@@ -77,7 +77,7 @@ module Gitlab
logger.log_timed(LOG_MESSAGES[__method__]) do
path_validations.each do |validation|
if error = validation.call(file_paths)
raise ::Gitlab::GitAccess::UnauthorizedError, error
raise ::Gitlab::GitAccess::ForbiddenError, error
end
end
end
......
......@@ -15,7 +15,7 @@ module Gitlab
lfs_check = Checks::LfsIntegrity.new(project, newrev, logger.time_left)
if lfs_check.objects_missing?
raise GitAccess::UnauthorizedError, ERROR_MESSAGE
raise GitAccess::ForbiddenError, ERROR_MESSAGE
end
end
end
......
......@@ -6,7 +6,7 @@ module Gitlab
def validate!
logger.log_timed("Checking if you are allowed to push...") do
unless can_push?
raise GitAccess::UnauthorizedError, GitAccess::ERROR_MESSAGES[:push_code]
raise GitAccess::ForbiddenError, GitAccess::ERROR_MESSAGES[:push_code]
end
end
end
......
......@@ -20,7 +20,7 @@ module Gitlab
logger.log_timed(LOG_MESSAGES[:tag_checks]) do
if tag_exists? && user_access.cannot_do_action?(:admin_tag)
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:change_existing_tags]
raise GitAccess::ForbiddenError, ERROR_MESSAGES[:change_existing_tags]
end
end
......@@ -33,11 +33,11 @@ module Gitlab
logger.log_timed(LOG_MESSAGES[__method__]) do
return unless ProtectedTag.protected?(project, tag_name) # rubocop:disable Cop/AvoidReturnFromBlocks
raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:update_protected_tag]) if update?
raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_protected_tag]) if deletion?
raise(GitAccess::ForbiddenError, ERROR_MESSAGES[:update_protected_tag]) if update?
raise(GitAccess::ForbiddenError, ERROR_MESSAGES[:delete_protected_tag]) if deletion?
unless user_access.can_create_tag?(tag_name)
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:create_protected_tag]
raise GitAccess::ForbiddenError, ERROR_MESSAGES[:create_protected_tag]
end
end
end
......
......@@ -322,6 +322,7 @@ module Gitlab
limit: 10,
offset: 0,
path: nil,
author: nil,
follow: false,
skip_merges: false,
after: nil,
......
......@@ -6,7 +6,7 @@ module Gitlab
class GitAccess
include Gitlab::Utils::StrongMemoize
UnauthorizedError = Class.new(StandardError)
ForbiddenError = Class.new(StandardError)
NotFoundError = Class.new(StandardError)
ProjectCreationError = Class.new(StandardError)
TimeoutError = Class.new(StandardError)
......@@ -125,7 +125,7 @@ module Gitlab
return unless actor.is_a?(Key)
unless actor.valid?
raise UnauthorizedError, "Your SSH key #{actor.errors[:key].first}."
raise ForbiddenError, "Your SSH key #{actor.errors[:key].first}."
end
end
......@@ -133,7 +133,7 @@ module Gitlab
return if request_from_ci_build?
unless protocol_allowed?
raise UnauthorizedError, "Git access over #{protocol.upcase} is not allowed"
raise ForbiddenError, "Git access over #{protocol.upcase} is not allowed"
end
end
......@@ -148,7 +148,7 @@ module Gitlab
unless user_access.allowed?
message = Gitlab::Auth::UserAccessDeniedReason.new(user).rejection_message
raise UnauthorizedError, message
raise ForbiddenError, message
end
end
......@@ -156,11 +156,11 @@ module Gitlab
case cmd
when *DOWNLOAD_COMMANDS
unless authentication_abilities.include?(:download_code) || authentication_abilities.include?(:build_download_code)
raise UnauthorizedError, ERROR_MESSAGES[:auth_download]
raise ForbiddenError, ERROR_MESSAGES[:auth_download]
end
when *PUSH_COMMANDS
unless authentication_abilities.include?(:push_code)
raise UnauthorizedError, ERROR_MESSAGES[:auth_upload]
raise ForbiddenError, ERROR_MESSAGES[:auth_upload]
end
end
end
......@@ -189,19 +189,19 @@ module Gitlab
def check_upload_pack_disabled!
if http? && upload_pack_disabled_over_http?
raise UnauthorizedError, ERROR_MESSAGES[:upload_pack_disabled_over_http]
raise ForbiddenError, ERROR_MESSAGES[:upload_pack_disabled_over_http]
end
end
def check_receive_pack_disabled!
if http? && receive_pack_disabled_over_http?
raise UnauthorizedError, ERROR_MESSAGES[:receive_pack_disabled_over_http]
raise ForbiddenError, ERROR_MESSAGES[:receive_pack_disabled_over_http]
end
end
def check_command_existence!(cmd)
unless ALL_COMMANDS.include?(cmd)
raise UnauthorizedError, ERROR_MESSAGES[:command_not_allowed]
raise ForbiddenError, ERROR_MESSAGES[:command_not_allowed]
end
end
......@@ -209,7 +209,7 @@ module Gitlab
return unless receive_pack?(cmd)
if Gitlab::Database.read_only?
raise UnauthorizedError, push_to_read_only_message
raise ForbiddenError, push_to_read_only_message
end
end
......@@ -253,23 +253,23 @@ module Gitlab
guest_can_download_code?
unless passed
raise UnauthorizedError, ERROR_MESSAGES[:download]
raise ForbiddenError, ERROR_MESSAGES[:download]
end
end
def check_push_access!
if project.repository_read_only?
raise UnauthorizedError, ERROR_MESSAGES[:read_only]
raise ForbiddenError, ERROR_MESSAGES[:read_only]
end
if deploy_key?
unless deploy_key.can_push_to?(project)
raise UnauthorizedError, ERROR_MESSAGES[:deploy_key_upload]
raise ForbiddenError, ERROR_MESSAGES[:deploy_key_upload]
end
elsif user
# User access is verified in check_change_access!
else
raise UnauthorizedError, ERROR_MESSAGES[:upload]
raise ForbiddenError, ERROR_MESSAGES[:upload]
end
check_change_access!
......@@ -284,7 +284,7 @@ module Gitlab
project.any_branch_allows_collaboration?(user_access.user)
unless can_push
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_code]
raise ForbiddenError, ERROR_MESSAGES[:push_code]
end
else
# If there are worktrees with a HEAD pointing to a non-existent object,
......
......@@ -19,11 +19,11 @@ module Gitlab
def check_change_access!
unless user_access.can_do_action?(:create_wiki)
raise UnauthorizedError, ERROR_MESSAGES[:write_to_wiki]
raise ForbiddenError, ERROR_MESSAGES[:write_to_wiki]
end
if Gitlab::Database.read_only?
raise UnauthorizedError, push_to_read_only_message
raise ForbiddenError, push_to_read_only_message
end
true
......
......@@ -324,6 +324,7 @@ module Gitlab
request.after = GitalyClient.timestamp(options[:after]) if options[:after]
request.before = GitalyClient.timestamp(options[:before]) if options[:before]
request.revision = encode_binary(options[:ref]) if options[:ref]
request.author = encode_binary(options[:author]) if options[:author]
request.order = options[:order].upcase.sub('DEFAULT', 'NONE') if options[:order].present?
request.paths = encode_repeated(Array(options[:path])) if options[:path].present?
......
......@@ -43,7 +43,7 @@ module Gitlab
end
def config_file
Rails.root.join('lib/gitlab/import_export/import_export.yml')
Rails.root.join('lib/gitlab/import_export/project/import_export.yml')
end
def version_filename
......@@ -77,7 +77,7 @@ module Gitlab
end
def group_config_file
Rails.root.join('lib/gitlab/import_export/group_import_export.yml')
Rails.root.join('lib/gitlab/import_export/group/import_export.yml')
end
end
end
......
......@@ -4,8 +4,8 @@ module Gitlab
module ImportExport
class AttributeCleaner
ALLOWED_REFERENCES = [
*ProjectRelationFactory::PROJECT_REFERENCES,
*ProjectRelationFactory::USER_REFERENCES,
*Gitlab::ImportExport::Project::RelationFactory::PROJECT_REFERENCES,
*Gitlab::ImportExport::Project::RelationFactory::USER_REFERENCES,
'group_id',
'commit_id',
'discussion_id',
......
# frozen_string_literal: true
module Gitlab
module ImportExport
module Base
# 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 ObjectBuilder
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
end
# frozen_string_literal: true
module Gitlab
module ImportExport
module Base
class RelationFactory
include Gitlab::Utils::StrongMemoize
IMPORTED_OBJECT_MAX_RETRIES = 5.freeze
OVERRIDES = {}.freeze
EXISTING_OBJECT_RELATIONS = %i[].freeze
# This represents all relations that have unique key on `project_id` or `group_id`
UNIQUE_RELATIONS = %i[].freeze
USER_REFERENCES = %w[
author_id
assignee_id
updated_by_id
merged_by_id
latest_closed_by_id
user_id
created_by_id
last_edited_by_id
merge_user_id
resolved_by_id
closed_by_id
owner_id
].freeze
TOKEN_RESET_MODELS = %i[Project Namespace Group Ci::Trigger Ci::Build Ci::Runner ProjectHook].freeze
def self.create(*args)
new(*args).create
end
def self.relation_class(relation_name)
# There are scenarios where the model is pluralized (e.g.
# MergeRequest::Metrics), and we don't want to force it to singular
# with #classify.
relation_name.to_s.classify.constantize
rescue NameError
relation_name.to_s.constantize
end
def initialize(relation_sym:, relation_hash:, members_mapper:, object_builder:, user:, importable:, excluded_keys: [])
@relation_name = self.class.overrides[relation_sym]&.to_sym || relation_sym
@relation_hash = relation_hash.except('noteable_id')
@members_mapper = members_mapper
@object_builder = object_builder
@user = user
@importable = importable
@imported_object_retries = 0
@relation_hash[importable_column_name] = @importable.id
# Remove excluded keys from relation_hash
# We don't do this in the parsed_relation_hash because of the 'transformed attributes'
# For example, MergeRequestDiffFiles exports its diff attribute as utf8_diff. Then,
# in the create method that attribute is renamed to diff. And because diff is an excluded key,
# if we clean the excluded keys in the parsed_relation_hash, it will be removed
# from the object attributes and the export will fail.
@relation_hash.except!(*excluded_keys)
end
# Creates an object from an actual model with name "relation_sym" with params from
# the relation_hash, updating references with new object IDs, mapping users using
# the "members_mapper" object, also updating notes if required.
def create
return if invalid_relation?
setup_base_models
setup_models
generate_imported_object
end
def self.overrides
self::OVERRIDES
end
def self.existing_object_relations
self::EXISTING_OBJECT_RELATIONS
end
private
def invalid_relation?
false
end
def setup_models
raise NotImplementedError
end
def unique_relations
# define in sub-class if any
self.class::UNIQUE_RELATIONS
end
def setup_base_models
update_user_references
remove_duplicate_assignees
reset_tokens!
remove_encrypted_attributes!
end
def update_user_references
self.class::USER_REFERENCES.each do |reference|
if @relation_hash[reference]
@relation_hash[reference] = @members_mapper.map[@relation_hash[reference]]
end
end
end
def remove_duplicate_assignees
return unless @relation_hash['issue_assignees']
# When an assignee did not exist in the members mapper, the importer is
# assigned. We only need to assign each user once.
@relation_hash['issue_assignees'].uniq!(&:user_id)
end
def generate_imported_object
imported_object
end
def reset_tokens!
return unless Gitlab::ImportExport.reset_tokens? && self.class::TOKEN_RESET_MODELS.include?(@relation_name)
# If we import/export to the same instance, tokens will have to be reset.
# We also have to reset them to avoid issues when the gitlab secrets file cannot be copied across.
relation_class.attribute_names.select { |name| name.include?('token') }.each do |token|
@relation_hash[token] = nil
end
end
def remove_encrypted_attributes!
return unless relation_class.respond_to?(:encrypted_attributes) && relation_class.encrypted_attributes.any?
relation_class.encrypted_attributes.each_key do |key|
@relation_hash[key.to_s] = nil
end
end
def relation_class
@relation_class ||= self.class.relation_class(@relation_name)
end
def importable_column_name
importable_class_name.concat('_id')
end
def importable_class_name
@importable.class.to_s.downcase
end
def imported_object
if existing_or_new_object.respond_to?(:importing)
existing_or_new_object.importing = true
end
existing_or_new_object
rescue ActiveRecord::RecordNotUnique
# as the operation is not atomic, retry in the unlikely scenario an INSERT is
# performed on the same object between the SELECT and the INSERT
@imported_object_retries += 1
retry if @imported_object_retries < IMPORTED_OBJECT_MAX_RETRIES
end
def parsed_relation_hash
@parsed_relation_hash ||= Gitlab::ImportExport::AttributeCleaner.clean(relation_hash: @relation_hash,
relation_class: relation_class)
end
def existing_or_new_object
# Only find existing records to avoid mapping tables such as milestones
# Otherwise always create the record, skipping the extra SELECT clause.
@existing_or_new_object ||= begin
if existing_object?
attribute_hash = attribute_hash_for(['events'])
existing_object.assign_attributes(attribute_hash) if attribute_hash.any?
existing_object
else
# Because of single-type inheritance, we need to be careful to use the `type` field
# See https://gitlab.com/gitlab-org/gitlab/issues/34860#note_235321497
inheritance_column = relation_class.try(:inheritance_column)
inheritance_attributes = parsed_relation_hash.slice(inheritance_column)
object = relation_class.new(inheritance_attributes)
object.assign_attributes(parsed_relation_hash)
object
end
end
end
def attribute_hash_for(attributes)
attributes.each_with_object({}) do |hash, value|
hash[value] = parsed_relation_hash.delete(value) if parsed_relation_hash[value]
hash
end
end
def existing_object
@existing_object ||= find_or_create_object!
end
def unique_relation_object
unique_relation_object = relation_class.find_or_create_by(importable_column_name => @importable.id)
unique_relation_object.assign_attributes(parsed_relation_hash)
unique_relation_object
end
def find_or_create_object!
return unique_relation_object if unique_relation?
# Can't use IDs as validation exists calling `group` or `project` attributes
finder_hash = parsed_relation_hash.tap do |hash|
if relation_class.attribute_method?('group_id') && @importable.is_a?(::Project)
hash['group'] = @importable.group
end
hash[importable_class_name] = @importable if relation_class.reflect_on_association(importable_class_name.to_sym)
hash.delete(importable_column_name)
end
@object_builder.build(relation_class, finder_hash)
end
def setup_note
set_note_author
# attachment is deprecated and note uploads are handled by Markdown uploader
@relation_hash['attachment'] = nil
end
# Sets the author for a note. If the user importing the project
# has admin access, an actual mapping with new project members
# will be used. Otherwise, a note stating the original author name
# is left.
def set_note_author
old_author_id = @relation_hash['author_id']
author = @relation_hash.delete('author')
update_note_for_missing_author(author['name']) unless has_author?(old_author_id)
end
def has_author?(old_author_id)
admin_user? && @members_mapper.include?(old_author_id)
end
def missing_author_note(updated_at, author_name)
timestamp = updated_at.split('.').first
"\n\n *By #{author_name} on #{timestamp} (imported from GitLab project)*"
end
def update_note_for_missing_author(author_name)
@relation_hash['note'] = '*Blank note*' if @relation_hash['note'].blank?
@relation_hash['note'] = "#{@relation_hash['note']}#{missing_author_note(@relation_hash['updated_at'], author_name)}"
end
def admin_user?
@user.admin?
end
def existing_object?
strong_memoize(:_existing_object) do
self.class.existing_object_relations.include?(@relation_name) || unique_relation?
end
end
def unique_relation?
strong_memoize(:unique_relation) do
importable_foreign_key.present? &&
(has_unique_index_on_importable_fk? || uses_importable_fk_as_primary_key?)
end
end
def has_unique_index_on_importable_fk?
cache = cached_has_unique_index_on_importable_fk
table_name = relation_class.table_name
return cache[table_name] if cache.has_key?(table_name)
index_exists =
ActiveRecord::Base.connection.index_exists?(
relation_class.table_name,
importable_foreign_key,
unique: true)
cache[table_name] = index_exists
end
# Avoid unnecessary DB requests
def cached_has_unique_index_on_importable_fk
Thread.current[:cached_has_unique_index_on_importable_fk] ||= {}
end
def uses_importable_fk_as_primary_key?
relation_class.primary_key == importable_foreign_key
end
def importable_foreign_key
relation_class.reflect_on_association(importable_class_name.to_sym)&.foreign_key
end
end
end
end
end
# 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
# frozen_string_literal: true
module Gitlab
module ImportExport
class BaseRelationFactory
include Gitlab::Utils::StrongMemoize
IMPORTED_OBJECT_MAX_RETRIES = 5.freeze
OVERRIDES = {}.freeze
EXISTING_OBJECT_RELATIONS = %i[].freeze
# This represents all relations that have unique key on `project_id` or `group_id`
UNIQUE_RELATIONS = %i[].freeze
USER_REFERENCES = %w[
author_id
assignee_id
updated_by_id
merged_by_id
latest_closed_by_id
user_id
created_by_id
last_edited_by_id
merge_user_id
resolved_by_id
closed_by_id
owner_id
].freeze
TOKEN_RESET_MODELS = %i[Project Namespace Group Ci::Trigger Ci::Build Ci::Runner ProjectHook].freeze
def self.create(*args)
new(*args).create
end
def self.relation_class(relation_name)
# There are scenarios where the model is pluralized (e.g.
# MergeRequest::Metrics), and we don't want to force it to singular
# with #classify.
relation_name.to_s.classify.constantize
rescue NameError
relation_name.to_s.constantize
end
def initialize(relation_sym:, relation_hash:, members_mapper:, object_builder:, user:, importable:, excluded_keys: [])
@relation_name = self.class.overrides[relation_sym]&.to_sym || relation_sym
@relation_hash = relation_hash.except('noteable_id')
@members_mapper = members_mapper
@object_builder = object_builder
@user = user
@importable = importable
@imported_object_retries = 0
@relation_hash[importable_column_name] = @importable.id
# Remove excluded keys from relation_hash
# We don't do this in the parsed_relation_hash because of the 'transformed attributes'
# For example, MergeRequestDiffFiles exports its diff attribute as utf8_diff. Then,
# in the create method that attribute is renamed to diff. And because diff is an excluded key,
# if we clean the excluded keys in the parsed_relation_hash, it will be removed
# from the object attributes and the export will fail.
@relation_hash.except!(*excluded_keys)
end
# Creates an object from an actual model with name "relation_sym" with params from
# the relation_hash, updating references with new object IDs, mapping users using
# the "members_mapper" object, also updating notes if required.
def create
return if invalid_relation?
setup_base_models
setup_models
generate_imported_object
end
def self.overrides
self::OVERRIDES
end
def self.existing_object_relations
self::EXISTING_OBJECT_RELATIONS
end
private
def invalid_relation?
false
end
def setup_models
raise NotImplementedError
end
def unique_relations
# define in sub-class if any
self.class::UNIQUE_RELATIONS
end
def setup_base_models
update_user_references
remove_duplicate_assignees
reset_tokens!
remove_encrypted_attributes!
end
def update_user_references
self.class::USER_REFERENCES.each do |reference|
if @relation_hash[reference]
@relation_hash[reference] = @members_mapper.map[@relation_hash[reference]]
end
end
end
def remove_duplicate_assignees
return unless @relation_hash['issue_assignees']
# When an assignee did not exist in the members mapper, the importer is
# assigned. We only need to assign each user once.
@relation_hash['issue_assignees'].uniq!(&:user_id)
end
def generate_imported_object
imported_object
end
def reset_tokens!
return unless Gitlab::ImportExport.reset_tokens? && self.class::TOKEN_RESET_MODELS.include?(@relation_name)
# If we import/export to the same instance, tokens will have to be reset.
# We also have to reset them to avoid issues when the gitlab secrets file cannot be copied across.
relation_class.attribute_names.select { |name| name.include?('token') }.each do |token|
@relation_hash[token] = nil
end
end
def remove_encrypted_attributes!
return unless relation_class.respond_to?(:encrypted_attributes) && relation_class.encrypted_attributes.any?
relation_class.encrypted_attributes.each_key do |key|
@relation_hash[key.to_s] = nil
end
end
def relation_class
@relation_class ||= self.class.relation_class(@relation_name)
end
def importable_column_name
importable_class_name.concat('_id')
end
def importable_class_name
@importable.class.to_s.downcase
end
def imported_object
if existing_or_new_object.respond_to?(:importing)
existing_or_new_object.importing = true
end
existing_or_new_object
rescue ActiveRecord::RecordNotUnique
# as the operation is not atomic, retry in the unlikely scenario an INSERT is
# performed on the same object between the SELECT and the INSERT
@imported_object_retries += 1
retry if @imported_object_retries < IMPORTED_OBJECT_MAX_RETRIES
end
def parsed_relation_hash
@parsed_relation_hash ||= Gitlab::ImportExport::AttributeCleaner.clean(relation_hash: @relation_hash,
relation_class: relation_class)
end
def existing_or_new_object
# Only find existing records to avoid mapping tables such as milestones
# Otherwise always create the record, skipping the extra SELECT clause.
@existing_or_new_object ||= begin
if existing_object?
attribute_hash = attribute_hash_for(['events'])
existing_object.assign_attributes(attribute_hash) if attribute_hash.any?
existing_object
else
# Because of single-type inheritance, we need to be careful to use the `type` field
# See https://gitlab.com/gitlab-org/gitlab/issues/34860#note_235321497
inheritance_column = relation_class.try(:inheritance_column)
inheritance_attributes = parsed_relation_hash.slice(inheritance_column)
object = relation_class.new(inheritance_attributes)
object.assign_attributes(parsed_relation_hash)
object
end
end
end
def attribute_hash_for(attributes)
attributes.each_with_object({}) do |hash, value|
hash[value] = parsed_relation_hash.delete(value) if parsed_relation_hash[value]
hash
end
end
def existing_object
@existing_object ||= find_or_create_object!
end
def unique_relation_object
unique_relation_object = relation_class.find_or_create_by(importable_column_name => @importable.id)
unique_relation_object.assign_attributes(parsed_relation_hash)
unique_relation_object
end
def find_or_create_object!
return unique_relation_object if unique_relation?
# Can't use IDs as validation exists calling `group` or `project` attributes
finder_hash = parsed_relation_hash.tap do |hash|
if relation_class.attribute_method?('group_id') && @importable.is_a?(Project)
hash['group'] = @importable.group
end
hash[importable_class_name] = @importable if relation_class.reflect_on_association(importable_class_name.to_sym)
hash.delete(importable_column_name)
end
@object_builder.build(relation_class, finder_hash)
end
def setup_note
set_note_author
# attachment is deprecated and note uploads are handled by Markdown uploader
@relation_hash['attachment'] = nil
end
# Sets the author for a note. If the user importing the project
# has admin access, an actual mapping with new project members
# will be used. Otherwise, a note stating the original author name
# is left.
def set_note_author
old_author_id = @relation_hash['author_id']
author = @relation_hash.delete('author')
update_note_for_missing_author(author['name']) unless has_author?(old_author_id)
end
def has_author?(old_author_id)
admin_user? && @members_mapper.include?(old_author_id)
end
def missing_author_note(updated_at, author_name)
timestamp = updated_at.split('.').first
"\n\n *By #{author_name} on #{timestamp} (imported from GitLab project)*"
end
def update_note_for_missing_author(author_name)
@relation_hash['note'] = '*Blank note*' if @relation_hash['note'].blank?
@relation_hash['note'] = "#{@relation_hash['note']}#{missing_author_note(@relation_hash['updated_at'], author_name)}"
end
def admin_user?
@user.admin?
end
def existing_object?
strong_memoize(:_existing_object) do
self.class.existing_object_relations.include?(@relation_name) || unique_relation?
end
end
def unique_relation?
strong_memoize(:unique_relation) do
importable_foreign_key.present? &&
(has_unique_index_on_importable_fk? || uses_importable_fk_as_primary_key?)
end
end
def has_unique_index_on_importable_fk?
cache = cached_has_unique_index_on_importable_fk
table_name = relation_class.table_name
return cache[table_name] if cache.has_key?(table_name)
index_exists =
ActiveRecord::Base.connection.index_exists?(
relation_class.table_name,
importable_foreign_key,
unique: true)
cache[table_name] = index_exists
end
# Avoid unnecessary DB requests
def cached_has_unique_index_on_importable_fk
Thread.current[:cached_has_unique_index_on_importable_fk] ||= {}
end
def uses_importable_fk_as_primary_key?
relation_class.primary_key == importable_foreign_key
end
def importable_foreign_key
relation_class.reflect_on_association(importable_class_name.to_sym)&.foreign_key
end
end
end
end
# frozen_string_literal: true
module Gitlab
module ImportExport
module Group
# Given a class, it finds or creates a new object at group level.
#
# Example:
# `Group::ObjectBuilder.build(Label, label_attributes)`
# finds or initializes a label with the given attributes.
class ObjectBuilder < Base::ObjectBuilder
def self.build(*args)
::Group.transaction do
super
end
end
def initialize(klass, attributes)
super
@group = @attributes['group']
update_description
end
private
attr_reader :group
# Convert description empty string to nil
# due to existing object being saved with description: nil
# Which makes object lookup to fail since nil != ''
def update_description
attributes['description'] = nil if attributes['description'] == ''
end
def where_clauses
[
where_clause_base,
where_clause_for_title,
where_clause_for_description,
where_clause_for_created_at
].compact
end
# Returns Arel clause `"{table_name}"."group_id" = {group.id}`
def where_clause_base
table[:group_id].in(group_and_ancestor_ids)
end
def group_and_ancestor_ids
group.ancestors.map(&:id) << group.id
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module ImportExport
module Group
class RelationFactory < Base::RelationFactory
OVERRIDES = {
labels: :group_labels,
priorities: :label_priorities,
label: :group_label,
parent: :epic
}.freeze
EXISTING_OBJECT_RELATIONS = %i[
epic
epics
milestone
milestones
label
labels
group_label
group_labels
].freeze
private
def setup_models
setup_note if @relation_name == :notes
update_group_references
end
def update_group_references
return unless self.class.existing_object_relations.include?(@relation_name)
return unless @relation_hash['group_id']
@relation_hash['group_id'] = @importable.id
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module ImportExport
module Group
class TreeRestorer
attr_reader :user
attr_reader :shared
attr_reader :group
def initialize(user:, shared:, group:, group_hash:)
@path = File.join(shared.export_path, 'group.json')
@user = user
@shared = shared
@group = group
@group_hash = group_hash
end
def restore
@tree_hash = @group_hash || read_tree_hash
@group_members = @tree_hash.delete('members')
@children = @tree_hash.delete('children')
if members_mapper.map && restorer.restore
@children&.each do |group_hash|
group = create_group(group_hash: group_hash, parent_group: @group)
shared = Gitlab::ImportExport::Shared.new(group)
self.class.new(
user: @user,
shared: shared,
group: group,
group_hash: group_hash
).restore
end
end
return false if @shared.errors.any?
true
rescue => e
@shared.error(e)
false
end
private
def read_tree_hash
json = IO.read(@path)
ActiveSupport::JSON.decode(json)
rescue => e
@shared.logger.error(
group_id: @group.id,
group_name: @group.name,
message: "Import/Export error: #{e.message}"
)
raise Gitlab::ImportExport::Error.new('Incorrect JSON format')
end
def restorer
@relation_tree_restorer ||= RelationTreeRestorer.new(
user: @user,
shared: @shared,
importable: @group,
tree_hash: @tree_hash.except('name', 'path'),
members_mapper: members_mapper,
object_builder: object_builder,
relation_factory: relation_factory,
reader: reader
)
end
def create_group(group_hash:, parent_group:)
group_params = {
name: group_hash['name'],
path: group_hash['path'],
parent_id: parent_group&.id,
visibility_level: sub_group_visibility_level(group_hash, parent_group)
}
::Groups::CreateService.new(@user, group_params).execute
end
def sub_group_visibility_level(group_hash, parent_group)
original_visibility_level = group_hash['visibility_level'] || Gitlab::VisibilityLevel::PRIVATE
if parent_group && parent_group.visibility_level < original_visibility_level
Gitlab::VisibilityLevel.closest_allowed_level(parent_group.visibility_level)
else
original_visibility_level
end
end
def members_mapper
@members_mapper ||= Gitlab::ImportExport::MembersMapper.new(exported_members: @group_members, user: @user, importable: @group)
end
def relation_factory
Gitlab::ImportExport::Group::RelationFactory
end
def object_builder
Gitlab::ImportExport::Group::ObjectBuilder
end
def reader
@reader ||= Gitlab::ImportExport::Reader.new(
shared: @shared,
config: Gitlab::ImportExport::Config.new(
config: Gitlab::ImportExport.group_config_file
).to_h
)
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module ImportExport
module Group
class TreeSaver
attr_reader :full_path, :shared
def initialize(group:, current_user:, shared:, params: {})
@params = params
@current_user = current_user
@shared = shared
@group = group
@full_path = File.join(@shared.export_path, ImportExport.group_filename)
end
def save
group_tree = serialize(@group, reader.group_tree)
tree_saver.save(group_tree, @shared.export_path, ImportExport.group_filename)
true
rescue => e
@shared.error(e)
false
end
private
def serialize(group, relations_tree)
group_tree = tree_saver.serialize(group, relations_tree)
group.children.each do |child|
group_tree['children'] ||= []
group_tree['children'] << serialize(child, relations_tree)
end
group_tree
rescue => e
@shared.error(e)
end
def reader
@reader ||= Gitlab::ImportExport::Reader.new(
shared: @shared,
config: Gitlab::ImportExport::Config.new(
config: Gitlab::ImportExport.group_config_file
).to_h
)
end
def tree_saver
@tree_saver ||= RelationTreeSaver.new
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module ImportExport
# Given a class, it finds or creates a new object at group level.
#
# Example:
# `GroupObjectBuilder.build(Label, label_attributes)`
# finds or initializes a label with the given attributes.
class GroupObjectBuilder < BaseObjectBuilder
def self.build(*args)
Group.transaction do
super
end
end
def initialize(klass, attributes)
super
@group = @attributes['group']
update_description
end
private
attr_reader :group
# Convert description empty string to nil
# due to existing object being saved with description: nil
# Which makes object lookup to fail since nil != ''
def update_description
attributes['description'] = nil if attributes['description'] == ''
end
def where_clauses
[
where_clause_base,
where_clause_for_title,
where_clause_for_description,
where_clause_for_created_at
].compact
end
# Returns Arel clause `"{table_name}"."group_id" = {group.id}`
def where_clause_base
table[:group_id].in(group_and_ancestor_ids)
end
def group_and_ancestor_ids
group.ancestors.map(&:id) << group.id
end
end
end
end
# frozen_string_literal: true
module Gitlab
module ImportExport
# Given a class, it finds or creates a new object
# (initializes in the case of Label) at group or project level.
# If it does not exist in the group, it creates it at project level.
#
# Example:
# `GroupProjectObjectBuilder.build(Label, label_attributes)`
# finds or initializes a label with the given attributes.
#
# It also adds some logic around Group Labels/Milestones for edge cases.
class GroupProjectObjectBuilder < BaseObjectBuilder
def self.build(*args)
Project.transaction do
super
end
end
def initialize(klass, attributes)
super
@group = @attributes['group']
@project = @attributes['project']
end
def find
return if epic? && group.nil?
super
end
private
attr_reader :group, :project
def where_clauses
[
where_clause_base,
where_clause_for_title,
where_clause_for_klass
].compact
end
# Returns Arel clause `"{table_name}"."project_id" = {project.id}` if project is present
# For example: merge_request has :target_project_id, and we are searching by :iid
# or, if group is present:
# `"{table_name}"."project_id" = {project.id} OR "{table_name}"."group_id" = {group.id}`
def where_clause_base
[].tap do |clauses|
clauses << table[:project_id].eq(project.id) if project
clauses << table[:group_id].in(group.self_and_ancestors_ids) if group
end.reduce(:or)
end
# Returns Arel clause for a particular model or `nil`.
def where_clause_for_klass
attrs_to_arel(attributes.slice('iid')) if merge_request?
end
def prepare_attributes
attributes.dup.tap do |atts|
atts.delete('group') unless epic?
if label?
atts['type'] = 'ProjectLabel' # Always create project labels
elsif milestone?
if atts['group_id'] # Transform new group milestones into project ones
atts['iid'] = nil
atts.delete('group_id')
else
claim_iid
end
end
atts['importing'] = true if klass.ancestors.include?(Importable)
end
end
def label?
klass == Label
end
def milestone?
klass == Milestone
end
def merge_request?
klass == MergeRequest
end
def epic?
klass == Epic
end
# If an existing group milestone used the IID
# claim the IID back and set the group milestone to use one available
# This is necessary to fix situations like the following:
# - Importing into a user namespace project with exported group milestones
# where the IID of the Group milestone could conflict with a project one.
def claim_iid
# The milestone has to be a group milestone, as it's the only case where
# we set the IID as the maximum. The rest of them are fixed.
milestone = project.milestones.find_by(iid: attributes['iid'])
return unless milestone
milestone.iid = nil
milestone.ensure_project_iid!
milestone.save!
end
end
end
end
Gitlab::ImportExport::GroupProjectObjectBuilder.prepend_if_ee('EE::Gitlab::ImportExport::GroupProjectObjectBuilder')
# frozen_string_literal: true
module Gitlab
module ImportExport
class GroupRelationFactory < BaseRelationFactory
OVERRIDES = {
labels: :group_labels,
priorities: :label_priorities,
label: :group_label,
parent: :epic
}.freeze
EXISTING_OBJECT_RELATIONS = %i[
epic
epics
milestone
milestones
label
labels
group_label
group_labels
].freeze
private
def setup_models
setup_note if @relation_name == :notes
update_group_references
end
def update_group_references
return unless self.class.existing_object_relations.include?(@relation_name)
return unless @relation_hash['group_id']
@relation_hash['group_id'] = @importable.id
end
end
end
end
# frozen_string_literal: true
module Gitlab
module ImportExport
class GroupTreeRestorer
attr_reader :user
attr_reader :shared
attr_reader :group
def initialize(user:, shared:, group:, group_hash:)
@path = File.join(shared.export_path, 'group.json')
@user = user
@shared = shared
@group = group
@group_hash = group_hash
end
def restore
@tree_hash = @group_hash || read_tree_hash
@group_members = @tree_hash.delete('members')
@children = @tree_hash.delete('children')
if members_mapper.map && restorer.restore
@children&.each do |group_hash|
group = create_group(group_hash: group_hash, parent_group: @group)
shared = Gitlab::ImportExport::Shared.new(group)
self.class.new(
user: @user,
shared: shared,
group: group,
group_hash: group_hash
).restore
end
end
return false if @shared.errors.any?
true
rescue => e
@shared.error(e)
false
end
private
def read_tree_hash
json = IO.read(@path)
ActiveSupport::JSON.decode(json)
rescue => e
@shared.logger.error(
group_id: @group.id,
group_name: @group.name,
message: "Import/Export error: #{e.message}"
)
raise Gitlab::ImportExport::Error.new('Incorrect JSON format')
end
def restorer
@relation_tree_restorer ||= RelationTreeRestorer.new(
user: @user,
shared: @shared,
importable: @group,
tree_hash: @tree_hash.except('name', 'path'),
members_mapper: members_mapper,
object_builder: object_builder,
relation_factory: relation_factory,
reader: reader
)
end
def create_group(group_hash:, parent_group:)
group_params = {
name: group_hash['name'],
path: group_hash['path'],
parent_id: parent_group&.id,
visibility_level: sub_group_visibility_level(group_hash, parent_group)
}
::Groups::CreateService.new(@user, group_params).execute
end
def sub_group_visibility_level(group_hash, parent_group)
original_visibility_level = group_hash['visibility_level'] || Gitlab::VisibilityLevel::PRIVATE
if parent_group && parent_group.visibility_level < original_visibility_level
Gitlab::VisibilityLevel.closest_allowed_level(parent_group.visibility_level)
else
original_visibility_level
end
end
def members_mapper
@members_mapper ||= Gitlab::ImportExport::MembersMapper.new(exported_members: @group_members, user: @user, importable: @group)
end
def relation_factory
Gitlab::ImportExport::GroupRelationFactory
end
def object_builder
Gitlab::ImportExport::GroupObjectBuilder
end
def reader
@reader ||= Gitlab::ImportExport::Reader.new(
shared: @shared,
config: Gitlab::ImportExport::Config.new(
config: Gitlab::ImportExport.group_config_file
).to_h
)
end
end
end
end
# frozen_string_literal: true
module Gitlab
module ImportExport
class GroupTreeSaver
attr_reader :full_path, :shared
def initialize(group:, current_user:, shared:, params: {})
@params = params
@current_user = current_user
@shared = shared
@group = group
@full_path = File.join(@shared.export_path, ImportExport.group_filename)
end
def save
group_tree = serialize(@group, reader.group_tree)
tree_saver.save(group_tree, @shared.export_path, ImportExport.group_filename)
true
rescue => e
@shared.error(e)
false
end
private
def serialize(group, relations_tree)
group_tree = tree_saver.serialize(group, relations_tree)
group.children.each do |child|
group_tree['children'] ||= []
group_tree['children'] << serialize(child, relations_tree)
end
group_tree
rescue => e
@shared.error(e)
end
def reader
@reader ||= Gitlab::ImportExport::Reader.new(
shared: @shared,
config: Gitlab::ImportExport::Config.new(
config: Gitlab::ImportExport.group_config_file
).to_h
)
end
def tree_saver
@tree_saver ||= RelationTreeSaver.new
end
end
end
end
......@@ -49,7 +49,7 @@ module Gitlab
end
def project_tree
@project_tree ||= Gitlab::ImportExport::ProjectTreeRestorer.new(user: current_user,
@project_tree ||= Gitlab::ImportExport::Project::TreeRestorer.new(user: current_user,
shared: shared,
project: project)
end
......@@ -125,7 +125,7 @@ module Gitlab
def project_to_overwrite
strong_memoize(:project_to_overwrite) do
Project.find_by_full_path("#{project.namespace.full_path}/#{original_path}")
::Project.find_by_full_path("#{project.namespace.full_path}/#{original_path}")
end
end
end
......
......@@ -91,9 +91,9 @@ module Gitlab
def relation_class
case @importable
when Project
when ::Project
ProjectMember
when Group
when ::Group
GroupMember
end
end
......
# frozen_string_literal: true
module Gitlab
module ImportExport
module Project
# Given a class, it finds or creates a new object
# (initializes in the case of Label) at group or project level.
# If it does not exist in the group, it creates it at project level.
#
# Example:
# `ObjectBuilder.build(Label, label_attributes)`
# finds or initializes a label with the given attributes.
#
# It also adds some logic around Group Labels/Milestones for edge cases.
class ObjectBuilder < Base::ObjectBuilder
def self.build(*args)
::Project.transaction do
super
end
end
def initialize(klass, attributes)
super
@group = @attributes['group']
@project = @attributes['project']
end
def find
return if epic? && group.nil?
super
end
private
attr_reader :group, :project
def where_clauses
[
where_clause_base,
where_clause_for_title,
where_clause_for_klass
].compact
end
# Returns Arel clause `"{table_name}"."project_id" = {project.id}` if project is present
# For example: merge_request has :target_project_id, and we are searching by :iid
# or, if group is present:
# `"{table_name}"."project_id" = {project.id} OR "{table_name}"."group_id" = {group.id}`
def where_clause_base
[].tap do |clauses|
clauses << table[:project_id].eq(project.id) if project
clauses << table[:group_id].in(group.self_and_ancestors_ids) if group
end.reduce(:or)
end
# Returns Arel clause for a particular model or `nil`.
def where_clause_for_klass
attrs_to_arel(attributes.slice('iid')) if merge_request?
end
def prepare_attributes
attributes.dup.tap do |atts|
atts.delete('group') unless epic?
if label?
atts['type'] = 'ProjectLabel' # Always create project labels
elsif milestone?
if atts['group_id'] # Transform new group milestones into project ones
atts['iid'] = nil
atts.delete('group_id')
else
claim_iid
end
end
atts['importing'] = true if klass.ancestors.include?(Importable)
end
end
def label?
klass == Label
end
def milestone?
klass == Milestone
end
def merge_request?
klass == MergeRequest
end
def epic?
klass == Epic
end
# If an existing group milestone used the IID
# claim the IID back and set the group milestone to use one available
# This is necessary to fix situations like the following:
# - Importing into a user namespace project with exported group milestones
# where the IID of the Group milestone could conflict with a project one.
def claim_iid
# The milestone has to be a group milestone, as it's the only case where
# we set the IID as the maximum. The rest of them are fixed.
milestone = project.milestones.find_by(iid: attributes['iid'])
return unless milestone
milestone.iid = nil
milestone.ensure_project_iid!
milestone.save!
end
end
end
end
end
Gitlab::ImportExport::Project::ObjectBuilder.prepend_if_ee('EE::Gitlab::ImportExport::Project::ObjectBuilder')
# frozen_string_literal: true
module Gitlab
module ImportExport
module Project
class RelationFactory < Base::RelationFactory
prepend_if_ee('::EE::Gitlab::ImportExport::Project::RelationFactory') # 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
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
@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
end
# frozen_string_literal: true
module Gitlab
module ImportExport
module Project
class TreeLoader
def load(path, dedup_entries: false)
tree_hash = ActiveSupport::JSON.decode(IO.read(path))
if dedup_entries
dedup_tree(tree_hash)
else
tree_hash
end
end
private
# This function removes duplicate entries from the given tree recursively
# by caching nodes it encounters repeatedly. We only consider nodes for
# which there can actually be multiple equivalent instances (e.g. strings,
# hashes and arrays, but not `nil`s, numbers or booleans.)
#
# The algorithm uses a recursive depth-first descent with 3 cases, starting
# with a root node (the tree/hash itself):
# - a node has already been cached; in this case we return it from the cache
# - a node has not been cached yet but should be; descend into its children
# - a node is neither cached nor qualifies for caching; this is a no-op
def dedup_tree(node, nodes_seen = {})
if nodes_seen.key?(node) && distinguishable?(node)
yield nodes_seen[node]
elsif should_dedup?(node)
nodes_seen[node] = node
case node
when Array
node.each_index do |idx|
dedup_tree(node[idx], nodes_seen) do |cached_node|
node[idx] = cached_node
end
end
when Hash
node.each do |k, v|
dedup_tree(v, nodes_seen) do |cached_node|
node[k] = cached_node
end
end
end
else
node
end
end
# We do not need to consider nodes for which there cannot be multiple instances
def should_dedup?(node)
node && !(node.is_a?(Numeric) || node.is_a?(TrueClass) || node.is_a?(FalseClass))
end
# We can only safely de-dup values that are distinguishable. True value objects
# are always distinguishable by nature. Hashes however can represent entities,
# which are identified by ID, not value. We therefore disallow de-duping hashes
# that do not have an `id` field, since we might risk dropping entities that
# have equal attributes yet different identities.
def distinguishable?(node)
if node.is_a?(Hash)
node.key?('id')
else
true
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module ImportExport
module Project
class TreeRestorer
LARGE_PROJECT_FILE_SIZE_BYTES = 500.megabyte
attr_reader :user
attr_reader :shared
attr_reader :project
def initialize(user:, shared:, project:)
@user = user
@shared = shared
@project = project
@tree_loader = TreeLoader.new
end
def restore
@tree_hash = read_tree_hash
@project_members = @tree_hash.delete('project_members')
RelationRenameService.rename(@tree_hash)
if relation_tree_restorer.restore
import_failure_service.with_retry(action: 'set_latest_merge_request_diff_ids!') do
@project.merge_requests.set_latest_merge_request_diff_ids!
end
true
else
false
end
rescue => e
@shared.error(e)
false
end
private
def large_project?(path)
File.size(path) >= LARGE_PROJECT_FILE_SIZE_BYTES
end
def read_tree_hash
path = File.join(@shared.export_path, 'project.json')
dedup_entries = large_project?(path) &&
Feature.enabled?(:dedup_project_import_metadata, project.group)
@tree_loader.load(path, dedup_entries: dedup_entries)
rescue => e
Rails.logger.error("Import/Export error: #{e.message}") # rubocop:disable Gitlab/RailsLogger
raise Gitlab::ImportExport::Error.new('Incorrect JSON format')
end
def relation_tree_restorer
@relation_tree_restorer ||= RelationTreeRestorer.new(
user: @user,
shared: @shared,
importable: @project,
tree_hash: @tree_hash,
object_builder: object_builder,
members_mapper: members_mapper,
relation_factory: relation_factory,
reader: reader
)
end
def members_mapper
@members_mapper ||= Gitlab::ImportExport::MembersMapper.new(exported_members: @project_members,
user: @user,
importable: @project)
end
def object_builder
Project::ObjectBuilder
end
def relation_factory
Project::RelationFactory
end
def reader
@reader ||= Gitlab::ImportExport::Reader.new(shared: @shared)
end
def import_failure_service
@import_failure_service ||= ImportFailureService.new(@project)
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module ImportExport
module Project
class TreeSaver
attr_reader :full_path
def initialize(project:, current_user:, shared:, params: {})
@params = params
@project = project
@current_user = current_user
@shared = shared
@full_path = File.join(@shared.export_path, ImportExport.project_filename)
end
def save
project_tree = tree_saver.serialize(@project, reader.project_tree)
fix_project_tree(project_tree)
tree_saver.save(project_tree, @shared.export_path, ImportExport.project_filename)
true
rescue => e
@shared.error(e)
false
end
private
# Aware that the resulting hash needs to be pure-hash and
# does not include any AR objects anymore, only objects that run `.to_json`
def fix_project_tree(project_tree)
if @params[:description].present?
project_tree['description'] = @params[:description]
end
project_tree['project_members'] += group_members_array
RelationRenameService.add_new_associations(project_tree)
end
def reader
@reader ||= Gitlab::ImportExport::Reader.new(shared: @shared)
end
def group_members_array
group_members.as_json(reader.group_members_tree).each do |group_member|
group_member['source_type'] = 'Project' # Make group members project members of the future import
end
end
def group_members
return [] unless @current_user.can?(:admin_group, @project.group)
# We need `.where.not(user_id: nil)` here otherwise when a group has an
# invitee, it would make the following query return 0 rows since a NULL
# user_id would be present in the subquery
# See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values
non_null_user_ids = @project.project_members.where.not(user_id: nil).select(:user_id)
GroupMembersFinder.new(@project.group).execute.where.not(user_id: non_null_user_ids)
end
def tree_saver
@tree_saver ||= RelationTreeSaver.new
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
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
@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
# frozen_string_literal: true
module Gitlab
module ImportExport
class ProjectTreeLoader
def load(path, dedup_entries: false)
tree_hash = ActiveSupport::JSON.decode(IO.read(path))
if dedup_entries
dedup_tree(tree_hash)
else
tree_hash
end
end
private
# This function removes duplicate entries from the given tree recursively
# by caching nodes it encounters repeatedly. We only consider nodes for
# which there can actually be multiple equivalent instances (e.g. strings,
# hashes and arrays, but not `nil`s, numbers or booleans.)
#
# The algorithm uses a recursive depth-first descent with 3 cases, starting
# with a root node (the tree/hash itself):
# - a node has already been cached; in this case we return it from the cache
# - a node has not been cached yet but should be; descend into its children
# - a node is neither cached nor qualifies for caching; this is a no-op
def dedup_tree(node, nodes_seen = {})
if nodes_seen.key?(node) && distinguishable?(node)
yield nodes_seen[node]
elsif should_dedup?(node)
nodes_seen[node] = node
case node
when Array
node.each_index do |idx|
dedup_tree(node[idx], nodes_seen) do |cached_node|
node[idx] = cached_node
end
end
when Hash
node.each do |k, v|
dedup_tree(v, nodes_seen) do |cached_node|
node[k] = cached_node
end
end
end
else
node
end
end
# We do not need to consider nodes for which there cannot be multiple instances
def should_dedup?(node)
node && !(node.is_a?(Numeric) || node.is_a?(TrueClass) || node.is_a?(FalseClass))
end
# We can only safely de-dup values that are distinguishable. True value objects
# are always distinguishable by nature. Hashes however can represent entities,
# which are identified by ID, not value. We therefore disallow de-duping hashes
# that do not have an `id` field, since we might risk dropping entities that
# have equal attributes yet different identities.
def distinguishable?(node)
if node.is_a?(Hash)
node.key?('id')
else
true
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module ImportExport
class ProjectTreeRestorer
LARGE_PROJECT_FILE_SIZE_BYTES = 500.megabyte
attr_reader :user
attr_reader :shared
attr_reader :project
def initialize(user:, shared:, project:)
@user = user
@shared = shared
@project = project
@tree_loader = ProjectTreeLoader.new
end
def restore
@tree_hash = read_tree_hash
@project_members = @tree_hash.delete('project_members')
RelationRenameService.rename(@tree_hash)
if relation_tree_restorer.restore
import_failure_service.with_retry(action: 'set_latest_merge_request_diff_ids!') do
@project.merge_requests.set_latest_merge_request_diff_ids!
end
true
else
false
end
rescue => e
@shared.error(e)
false
end
private
def large_project?(path)
File.size(path) >= LARGE_PROJECT_FILE_SIZE_BYTES
end
def read_tree_hash
path = File.join(@shared.export_path, 'project.json')
dedup_entries = large_project?(path) &&
Feature.enabled?(:dedup_project_import_metadata, project.group)
@tree_loader.load(path, dedup_entries: dedup_entries)
rescue => e
Rails.logger.error("Import/Export error: #{e.message}") # rubocop:disable Gitlab/RailsLogger
raise Gitlab::ImportExport::Error.new('Incorrect JSON format')
end
def relation_tree_restorer
@relation_tree_restorer ||= RelationTreeRestorer.new(
user: @user,
shared: @shared,
importable: @project,
tree_hash: @tree_hash,
object_builder: object_builder,
members_mapper: members_mapper,
relation_factory: relation_factory,
reader: reader
)
end
def members_mapper
@members_mapper ||= Gitlab::ImportExport::MembersMapper.new(exported_members: @project_members,
user: @user,
importable: @project)
end
def object_builder
Gitlab::ImportExport::GroupProjectObjectBuilder
end
def relation_factory
Gitlab::ImportExport::ProjectRelationFactory
end
def reader
@reader ||= Gitlab::ImportExport::Reader.new(shared: @shared)
end
def import_failure_service
@import_failure_service ||= ImportFailureService.new(@project)
end
end
end
end
# frozen_string_literal: true
module Gitlab
module ImportExport
class ProjectTreeSaver
attr_reader :full_path
def initialize(project:, current_user:, shared:, params: {})
@params = params
@project = project
@current_user = current_user
@shared = shared
@full_path = File.join(@shared.export_path, ImportExport.project_filename)
end
def save
project_tree = tree_saver.serialize(@project, reader.project_tree)
fix_project_tree(project_tree)
tree_saver.save(project_tree, @shared.export_path, ImportExport.project_filename)
true
rescue => e
@shared.error(e)
false
end
private
# Aware that the resulting hash needs to be pure-hash and
# does not include any AR objects anymore, only objects that run `.to_json`
def fix_project_tree(project_tree)
if @params[:description].present?
project_tree['description'] = @params[:description]
end
project_tree['project_members'] += group_members_array
RelationRenameService.add_new_associations(project_tree)
end
def reader
@reader ||= Gitlab::ImportExport::Reader.new(shared: @shared)
end
def group_members_array
group_members.as_json(reader.group_members_tree).each do |group_member|
group_member['source_type'] = 'Project' # Make group members project members of the future import
end
end
def group_members
return [] unless @current_user.can?(:admin_group, @project.group)
# We need `.where.not(user_id: nil)` here otherwise when a group has an
# invitee, it would make the following query return 0 rows since a NULL
# user_id would be present in the subquery
# See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values
non_null_user_ids = @project.project_members.where.not(user_id: nil).select(:user_id)
GroupMembersFinder.new(@project.group).execute.where.not(user_id: non_null_user_ids)
end
def tree_saver
@tree_saver ||= RelationTreeSaver.new
end
end
end
end
......@@ -69,7 +69,7 @@ module Gitlab
def process_relation_item!(relation_key, relation_definition, relation_index, data_hash)
relation_object = build_relation(relation_key, relation_definition, data_hash)
return unless relation_object
return if importable_class == Project && group_model?(relation_object)
return if importable_class == ::Project && group_model?(relation_object)
relation_object.assign_attributes(importable_class_sym => @importable)
......@@ -110,7 +110,7 @@ module Gitlab
excluded_keys: excluded_keys_for_relation(importable_class_sym))
@importable.assign_attributes(params)
@importable.drop_visibility_level! if importable_class == Project
@importable.drop_visibility_level! if importable_class == ::Project
Gitlab::Timeless.timeless(@importable) do
@importable.save!
......
......@@ -3,7 +3,6 @@
class GitlabDanger
LOCAL_RULES ||= %w[
changes_size
gemfile
documentation
frozen_string
duplicate_yarn_dependencies
......
#!/bin/sh
gemfile_lock_changed() {
if [ -n "$(git diff --name-only -- Gemfile.lock)" ]; then
cat << EOF
Gemfile was updated but Gemfile.lock was not updated.
Usually, when Gemfile is updated, you should run
\`\`\`
bundle install
\`\`\`
or
\`\`\`
bundle update <the-added-or-updated-gem>
\`\`\`
and commit the Gemfile.lock changes.
EOF
exit 1
fi
}
gemfile_lock_changed
......@@ -49,7 +49,8 @@ def jobs_to_run(node_index, node_total)
%w[scripts/lint-conflicts.sh],
%w[scripts/lint-rugged],
%w[scripts/frontend/check_no_partial_karma_jest.sh],
%w[scripts/lint-changelog-filenames]
%w[scripts/lint-changelog-filenames],
%w[scripts/gemfile_lock_changed.sh]
]
case node_total
......
......@@ -4,7 +4,7 @@ require 'spec_helper'
describe "Admin Health Check", :feature do
include StubENV
set(:admin) { create(:admin) }
let_it_be(:admin) { create(:admin) }
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
......
......@@ -3,11 +3,11 @@
require 'spec_helper'
describe 'Multiple Issue Boards', :js do
set(:user) { create(:user) }
set(:project) { create(:project, :public) }
set(:planning) { create(:label, project: project, name: 'Planning') }
set(:board) { create(:board, name: 'board1', project: project) }
set(:board2) { create(:board, name: 'board2', project: project) }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
let_it_be(:planning) { create(:label, project: project, name: 'Planning') }
let_it_be(:board) { create(:board, name: 'board1', project: project) }
let_it_be(:board2) { create(:board, name: 'board2', project: project) }
let(:parent) { project }
let(:boards_path) { project_boards_path(project) }
......
......@@ -129,10 +129,10 @@ describe 'Issue Boards new issue', :js do
end
context 'group boards' do
set(:group) { create(:group, :public) }
set(:project) { create(:project, namespace: group) }
set(:group_board) { create(:board, group: group) }
set(:list) { create(:list, board: group_board, position: 0) }
let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, namespace: group) }
let_it_be(:group_board) { create(:board, group: group) }
let_it_be(:list) { create(:list, board: group_board, position: 0) }
context 'for unauthorized users' do
before do
......
......@@ -3,8 +3,8 @@
require 'spec_helper'
describe 'Commits' do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
describe 'CI' do
before do
......@@ -183,4 +183,41 @@ describe 'Commits' do
expect(find('.js-project-refs-dropdown')).to have_content branch_name
end
end
context 'viewing commits for an author' do
let(:author_commit) { project.repository.commits(nil, limit: 1).first }
let(:commits) { project.repository.commits(nil, author: author, limit: 40) }
before do
project.add_maintainer(user)
sign_in(user)
visit project_commits_path(project, nil, author: author)
end
shared_examples 'show commits by author' do
it "includes the author's commits" do
commits.each do |commit|
expect(page).to have_content("#{author_commit.author_name} authored #{commit.authored_date.strftime("%b %d, %Y")}")
end
end
end
context 'author is complete' do
let(:author) { "#{author_commit.author_name} <#{author_commit.author_email}>" }
it_behaves_like 'show commits by author'
end
context 'author is just a name' do
let(:author) { "#{author_commit.author_name}" }
it_behaves_like 'show commits by author'
end
context 'author is just an email' do
let(:author) { "#{author_commit.author_email}" }
it_behaves_like 'show commits by author'
end
end
end
......@@ -3,17 +3,17 @@
require 'spec_helper'
describe 'Root explore' do
set(:public_project) { create(:project, :public) }
set(:archived_project) { create(:project, :archived) }
set(:internal_project) { create(:project, :internal) }
set(:private_project) { create(:project, :private) }
let_it_be(:public_project) { create(:project, :public) }
let_it_be(:archived_project) { create(:project, :archived) }
let_it_be(:internal_project) { create(:project, :internal) }
let_it_be(:private_project) { create(:project, :private) }
before do
allow(Gitlab).to receive(:com?).and_return(true)
end
context 'when logged in' do
set(:user) { create(:user) }
let_it_be(:user) { create(:user) }
before do
sign_in(user)
......
......@@ -3,10 +3,10 @@
require 'spec_helper'
describe 'User explores projects' do
set(:archived_project) { create(:project, :archived) }
set(:internal_project) { create(:project, :internal) }
set(:private_project) { create(:project, :private) }
set(:public_project) { create(:project, :public) }
let_it_be(:archived_project) { create(:project, :archived) }
let_it_be(:internal_project) { create(:project, :internal) }
let_it_be(:private_project) { create(:project, :private) }
let_it_be(:public_project) { create(:project, :public) }
context 'when not signed in' do
context 'when viewing public projects' do
......@@ -19,7 +19,7 @@ describe 'User explores projects' do
end
context 'when signed in' do
set(:user) { create(:user) }
let_it_be(:user) { create(:user) }
before do
sign_in(user)
......
......@@ -3,7 +3,7 @@
require 'spec_helper'
describe 'Groups > Labels > User sees links to issuables' do
set(:group) { create(:group, :public) }
let_it_be(:group) { create(:group, :public) }
before do
create(:group_label, group: group, title: 'bug')
......
......@@ -7,7 +7,7 @@ describe "User views issues" do
let!(:open_issue1) { create(:issue, project: project) }
let!(:open_issue2) { create(:issue, project: project) }
set(:user) { create(:user) }
let_it_be(:user) { create(:user) }
shared_examples "opens issue from list" do
it "opens issue" do
......
......@@ -5,8 +5,7 @@ require 'spec_helper'
describe 'Merge request > User posts notes', :js do
include NoteInteractionHelpers
set(:project) { create(:project, :repository) }
let_it_be(:project) { create(:project, :repository) }
let(:user) { project.creator }
let(:merge_request) do
create(:merge_request, source_project: project, target_project: project)
......
......@@ -10,10 +10,10 @@ describe 'User sorts merge requests' do
create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test')
end
set(:user) { create(:user) }
set(:group) { create(:group) }
set(:group_member) { create(:group_member, :maintainer, user: user, group: group) }
set(:project) { create(:project, :public, group: group) }
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:group_member) { create(:group_member, :maintainer, user: user, group: group) }
let_it_be(:project) { create(:project, :public, group: group) }
before do
sign_in(user)
......
......@@ -3,7 +3,7 @@
require 'spec_helper'
describe 'User views open merge requests' do
set(:user) { create(:user) }
let_it_be(:user) { create(:user) }
shared_examples_for 'shows merge requests' do
it 'shows merge requests' do
......@@ -12,7 +12,7 @@ describe 'User views open merge requests' do
end
context 'when project is public' do
set(:project) { create(:project, :public, :repository) }
let_it_be(:project) { create(:project, :public, :repository) }
context 'when not signed in' do
context "when the target branch is the project's default branch" do
......@@ -114,7 +114,7 @@ describe 'User views open merge requests' do
context 'when project is internal' do
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
set(:project) { create(:project, :internal, :repository) }
let_it_be(:project) { create(:project, :internal, :repository) }
context 'when signed in' do
before do
......
......@@ -3,8 +3,8 @@
require 'spec_helper'
describe "User creates milestone", :js do
set(:user) { create(:user) }
set(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
before do
project.add_developer(user)
......
......@@ -3,9 +3,9 @@
require 'spec_helper'
describe "User edits milestone", :js do
set(:user) { create(:user) }
set(:project) { create(:project) }
set(:milestone) { create(:milestone, project: project, start_date: Date.today, due_date: 5.days.from_now) }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:milestone) { create(:milestone, project: project, start_date: Date.today, due_date: 5.days.from_now) }
before do
project.add_developer(user)
......
......@@ -3,10 +3,10 @@
require 'spec_helper'
describe 'User promotes milestone' do
set(:group) { create(:group) }
set(:user) { create(:user) }
set(:project) { create(:project, namespace: group) }
set(:milestone) { create(:milestone, project: project) }
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: group) }
let_it_be(:milestone) { create(:milestone, project: project) }
context 'when user can admin group milestones' do
before do
......
......@@ -3,10 +3,10 @@
require 'spec_helper'
describe "User views milestone" do
set(:user) { create(:user) }
set(:project) { create(:project) }
set(:milestone) { create(:milestone, project: project) }
set(:labels) { create_list(:label, 2, project: project) }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:labels) { create_list(:label, 2, project: project) }
before do
project.add_developer(user)
......
......@@ -3,9 +3,9 @@
require 'spec_helper'
describe "User views milestones" do
set(:user) { create(:user) }
set(:project) { create(:project) }
set(:milestone) { create(:milestone, project: project) }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:milestone) { create(:milestone, project: project) }
before do
project.add_developer(user)
......@@ -22,8 +22,8 @@ describe "User views milestones" do
end
context "with issues" do
set(:issue) { create(:issue, project: project, milestone: milestone) }
set(:closed_issue) { create(:closed_issue, project: project, milestone: milestone) }
let_it_be(:issue) { create(:issue, project: project, milestone: milestone) }
let_it_be(:closed_issue) { create(:closed_issue, project: project, milestone: milestone) }
it "opens milestone" do
click_link(milestone.title)
......@@ -38,7 +38,7 @@ describe "User views milestones" do
end
context "with associated releases" do
set(:first_release) { create(:release, project: project, name: "The first release", milestones: [milestone], released_at: Time.zone.parse('2019-10-07')) }
let_it_be(:first_release) { create(:release, project: project, name: "The first release", milestones: [milestone], released_at: Time.zone.parse('2019-10-07')) }
context "with a single associated release" do
it "shows the associated release" do
......@@ -48,10 +48,10 @@ describe "User views milestones" do
end
context "with lots of associated releases" do
set(:second_release) { create(:release, project: project, name: "The second release", milestones: [milestone], released_at: first_release.released_at + 1.day) }
set(:third_release) { create(:release, project: project, name: "The third release", milestones: [milestone], released_at: second_release.released_at + 1.day) }
set(:fourth_release) { create(:release, project: project, name: "The fourth release", milestones: [milestone], released_at: third_release.released_at + 1.day) }
set(:fifth_release) { create(:release, project: project, name: "The fifth release", milestones: [milestone], released_at: fourth_release.released_at + 1.day) }
let_it_be(:second_release) { create(:release, project: project, name: "The second release", milestones: [milestone], released_at: first_release.released_at + 1.day) }
let_it_be(:third_release) { create(:release, project: project, name: "The third release", milestones: [milestone], released_at: second_release.released_at + 1.day) }
let_it_be(:fourth_release) { create(:release, project: project, name: "The fourth release", milestones: [milestone], released_at: third_release.released_at + 1.day) }
let_it_be(:fifth_release) { create(:release, project: project, name: "The fifth release", milestones: [milestone], released_at: fourth_release.released_at + 1.day) }
it "shows the associated releases and the truncation text" do
expect(page).to have_content("Releases #{fifth_release.name}#{fourth_release.name}#{third_release.name} • 2 more releases")
......@@ -66,9 +66,9 @@ describe "User views milestones" do
end
describe "User views milestones with no MR" do
set(:user) { create(:user) }
set(:project) { create(:project, :merge_requests_disabled) }
set(:milestone) { create(:milestone, project: project) }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :merge_requests_disabled) }
let_it_be(:milestone) { create(:milestone, project: project) }
before do
project.add_developer(user)
......
......@@ -3,9 +3,9 @@
require "spec_helper"
describe "User downloads artifacts" do
set(:project) { create(:project, :repository, :public) }
set(:pipeline) { create(:ci_empty_pipeline, status: :success, sha: project.commit.id, project: project) }
set(:job) { create(:ci_build, :artifacts, :success, pipeline: pipeline) }
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:pipeline) { create(:ci_empty_pipeline, status: :success, sha: project.commit.id, project: project) }
let_it_be(:job) { create(:ci_build, :artifacts, :success, pipeline: pipeline) }
shared_examples "downloading" do
it "downloads the zip" do
......
......@@ -3,7 +3,7 @@
require 'spec_helper'
describe 'Pipeline Badge' do
set(:project) { create(:project, :repository, :public) }
let_it_be(:project) { create(:project, :repository, :public) }
let(:ref) { project.default_branch }
context 'when the project has a pipeline' do
......
......@@ -3,7 +3,7 @@
require "spec_helper"
describe "User deletes branch", :js do
set(:user) { create(:user) }
let_it_be(:user) { create(:user) }
let(:project) { create(:project, :repository) }
before do
......
......@@ -3,8 +3,8 @@
require "spec_helper"
describe "User views branches" do
set(:project) { create(:project, :repository) }
set(:user) { project.owner }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.owner }
before do
sign_in(user)
......@@ -23,7 +23,7 @@ describe "User views branches" do
end
context "protected branches" do
set(:protected_branch) { create(:protected_branch, project: project) }
let_it_be(:protected_branch) { create(:protected_branch, project: project) }
before do
visit(project_protected_branches_path(project))
......
......@@ -5,8 +5,8 @@ require 'spec_helper'
describe 'Project > Commit > View user status' do
include RepoHelpers
set(:project) { create(:project, :repository) }
set(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let(:commit_author) { create(:user, email: sample_commit.author_email) }
before do
......
......@@ -3,8 +3,8 @@
require "spec_helper"
describe "User creates labels" do
set(:project) { create(:project_empty_repo, :public) }
set(:user) { create(:user) }
let_it_be(:project) { create(:project_empty_repo, :public) }
let_it_be(:user) { create(:user) }
shared_examples_for "label creation" do
it "creates new label" do
......@@ -66,7 +66,7 @@ describe "User creates labels" do
end
context "in another project" do
set(:another_project) { create(:project_empty_repo, :public) }
let_it_be(:another_project) { create(:project_empty_repo, :public) }
before do
create(:label, project: project, title: "bug") # Create label for `project` (not `another_project`) project.
......
......@@ -3,9 +3,9 @@
require "spec_helper"
describe "User edits labels" do
set(:project) { create(:project_empty_repo, :public) }
set(:label) { create(:label, project: project) }
set(:user) { create(:user) }
let_it_be(:project) { create(:project_empty_repo, :public) }
let_it_be(:label) { create(:label, project: project) }
let_it_be(:user) { create(:user) }
before do
project.add_maintainer(user)
......
......@@ -3,10 +3,10 @@
require 'spec_helper'
describe 'User promotes label' do
set(:group) { create(:group) }
set(:user) { create(:user) }
set(:project) { create(:project, namespace: group) }
set(:label) { create(:label, project: project) }
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: group) }
let_it_be(:label) { create(:label, project: project) }
context 'when user can admin group labels' do
before do
......
......@@ -3,7 +3,7 @@
require 'spec_helper'
describe 'Projects > Labels > User sees links to issuables' do
set(:user) { create(:user) }
let_it_be(:user) { create(:user) }
before do
label # creates the label
......@@ -50,7 +50,7 @@ describe 'Projects > Labels > User sees links to issuables' do
end
context 'with a group label' do
set(:group) { create(:group) }
let_it_be(:group) { create(:group) }
let(:label) { create(:group_label, group: group, title: 'bug') }
context 'when merge requests and issues are enabled for the project' do
......
......@@ -3,9 +3,8 @@
require "spec_helper"
describe "User views labels" do
set(:project) { create(:project_empty_repo, :public) }
set(:user) { create(:user) }
let_it_be(:project) { create(:project_empty_repo, :public) }
let_it_be(:user) { create(:user) }
let(:label_titles) { %w[bug enhancement feature] }
let!(:prioritized_label) { create(:label, project: project, title: 'prioritized-label-name', priority: 1) }
......
......@@ -3,7 +3,7 @@
require 'spec_helper'
describe 'Projects settings' do
set(:project) { create(:project) }
let_it_be(:project) { create(:project) }
let(:user) { project.owner }
let(:panel) { find('.general-settings', match: :first) }
let(:button) { panel.find('.btn.js-settings-toggle') }
......
......@@ -3,7 +3,7 @@
require 'spec_helper'
describe 'Projects > Show > User sees Git instructions' do
set(:user) { create(:user) }
let_it_be(:user) { create(:user) }
shared_examples_for 'redirects to the sign in page' do
it 'redirects to the sign in page' do
......@@ -49,7 +49,7 @@ describe 'Projects > Show > User sees Git instructions' do
context 'when project is public' do
context 'when project has no repo' do
set(:project) { create(:project, :public) }
let_it_be(:project) { create(:project, :public) }
before do
sign_in(project.owner)
......@@ -60,7 +60,7 @@ describe 'Projects > Show > User sees Git instructions' do
end
context 'when project is empty' do
set(:project) { create(:project_empty_repo, :public) }
let_it_be(:project) { create(:project_empty_repo, :public) }
context 'when not signed in' do
before do
......@@ -98,7 +98,7 @@ describe 'Projects > Show > User sees Git instructions' do
end
context 'when project is not empty' do
set(:project) { create(:project, :public, :repository) }
let_it_be(:project) { create(:project, :public, :repository) }
before do
visit(project_path(project))
......@@ -141,7 +141,7 @@ describe 'Projects > Show > User sees Git instructions' do
end
context 'when project is internal' do
set(:project) { create(:project, :internal, :repository) }
let_it_be(:project) { create(:project, :internal, :repository) }
context 'when not signed in' do
before do
......@@ -163,7 +163,7 @@ describe 'Projects > Show > User sees Git instructions' do
end
context 'when project is private' do
set(:project) { create(:project, :private) }
let_it_be(:project) { create(:project, :private) }
before do
visit(project_path(project))
......
......@@ -3,7 +3,7 @@
require 'spec_helper'
describe 'Projects > Show > User sees last commit CI status' do
set(:project) { create(:project, :repository, :public) }
let_it_be(:project) { create(:project, :repository, :public) }
it 'shows the project README', :js do
project.enable_ci
......
......@@ -3,9 +3,8 @@
require 'spec_helper'
describe 'Projects > Show > User sees README' do
set(:user) { create(:user) }
set(:project) { create(:project, :repository, :public) }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, :public) }
it 'shows the project README', :js do
visit project_path(project)
......
......@@ -3,8 +3,7 @@
require 'spec_helper'
describe 'User sees user popover', :js do
set(:project) { create(:project, :repository) }
let_it_be(:project) { create(:project, :repository) }
let(:user) { project.creator }
let(:merge_request) do
create(:merge_request, source_project: project, target_project: project)
......
......@@ -3,7 +3,7 @@
require 'spec_helper'
describe 'Projects > Wiki > User previews markdown changes', :js do
set(:user) { create(:user) }
let_it_be(:user) { create(:user) }
let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: '[some link](other-page)' }) }
let(:wiki_content) do
......
......@@ -3,6 +3,8 @@
require "spec_helper"
describe "User creates wiki page" do
include WikiHelpers
let(:user) { create(:user) }
let(:wiki) { ProjectWiki.new(project, user) }
let(:project) { create(:project) }
......@@ -14,9 +16,11 @@ describe "User creates wiki page" do
end
context "when wiki is empty" do
before do
before do |example|
visit(project_wikis_path(project))
wait_for_svg_to_be_loaded(example)
click_link "Create your first page"
end
......@@ -45,7 +49,7 @@ describe "User creates wiki page" do
expect(page).to have_content("Create New Page")
end
it "shows non-escaped link in the pages list", :quarantine do
it "shows non-escaped link in the pages list" do
fill_in(:wiki_title, with: "one/two/three-test")
page.within(".wiki-form") do
......@@ -163,7 +167,7 @@ describe "User creates wiki page" do
expect(page).to have_link('Link to Home', href: "/#{project.full_path}/-/wikis/home")
end
it_behaves_like 'wiki file attachments', :quarantine
it_behaves_like 'wiki file attachments'
end
context "in a group namespace", :js do
......@@ -175,7 +179,7 @@ describe "User creates wiki page" do
expect(page).to have_field("wiki[message]", with: "Create home")
end
it "creates a page from the home page", :quarantine do
it "creates a page from the home page" do
page.within(".wiki-form") do
fill_in(:wiki_content, with: "My awesome wiki!")
......
......@@ -19,9 +19,12 @@ describe 'User views a wiki page' do
sign_in(user)
end
context 'when wiki is empty' do
context 'when wiki is empty', :js do
before do
visit(project_wikis_path(project))
visit project_wikis_path(project)
wait_for_svg_to_be_loaded
click_link "Create your first page"
fill_in(:wiki_title, with: 'one/two/three-test')
......@@ -32,7 +35,7 @@ describe 'User views a wiki page' do
end
end
it 'shows the history of a page that has a path', :js do
it 'shows the history of a page that has a path' do
expect(current_path).to include('one/two/three-test')
first(:link, text: 'three').click
......@@ -45,7 +48,7 @@ describe 'User views a wiki page' do
end
end
it 'shows an old version of a page', :js do
it 'shows an old version of a page' do
expect(current_path).to include('one/two/three-test')
expect(find('.wiki-pages')).to have_content('three')
......@@ -162,9 +165,12 @@ describe 'User views a wiki page' do
end
it 'opens a default wiki page', :js do
visit(project_path(project))
visit project_path(project)
find('.shortcuts-wiki').click
wait_for_svg_to_be_loaded
click_link "Create your first page"
expect(page).to have_content('Create New Page')
......
......@@ -3,7 +3,7 @@
require 'spec_helper'
describe 'read-only message' do
set(:user) { create(:user) }
let_it_be(:user) { create(:user) }
before do
sign_in(user)
......
......@@ -5,7 +5,7 @@ require 'spec_helper'
describe "Internal Project Access" do
include AccessMatchers
set(:project) { create(:project, :internal, :repository) }
let_it_be(:project, reload: true) { create(:project, :internal, :repository) }
describe "Project should be internal" do
describe '#internal?' do
......
......@@ -5,7 +5,7 @@ require 'spec_helper'
describe "Private Project Access" do
include AccessMatchers
set(:project) { create(:project, :private, :repository, public_builds: false) }
let_it_be(:project, reload: true) { create(:project, :private, :repository, public_builds: false) }
describe "Project should be private" do
describe '#private?' do
......
......@@ -5,7 +5,7 @@ require 'spec_helper'
describe "Public Project Access" do
include AccessMatchers
set(:project) { create(:project, :public, :repository) }
let_it_be(:project, reload: true) { create(:project, :public, :repository) }
describe "Project should be public" do
describe '#public?' do
......
......@@ -10,10 +10,10 @@ describe "User sorts things" do
include Spec::Support::Helpers::Features::SortingHelpers
include DashboardHelper
set(:project) { create(:project_empty_repo, :public) }
set(:current_user) { create(:user) } # Using `current_user` instead of just `user` because of the hardoced call in `assigned_mrs_dashboard_path` which is used below.
set(:issue) { create(:issue, project: project, author: current_user) }
set(:merge_request) { create(:merge_request, target_project: project, source_project: project, author: current_user) }
let_it_be(:project) { create(:project_empty_repo, :public) }
let_it_be(:current_user) { create(:user) } # Using `current_user` instead of just `user` because of the hardoced call in `assigned_mrs_dashboard_path` which is used below.
let_it_be(:issue) { create(:issue, project: project, author: current_user) }
let_it_be(:merge_request) { create(:merge_request, target_project: project, source_project: project, author: current_user) }
before do
project.add_developer(current_user)
......
......@@ -1442,7 +1442,7 @@ TodoService
marks a single todo id as done
caches the number of todos of a user
Gitlab::ImportExport::ProjectTreeSaver
Gitlab::ImportExport::Project::TreeSaver
saves the project tree into a json object
saves project successfully
JSON
......
......@@ -15,7 +15,7 @@ describe Gitlab::Checks::BranchCheck do
let(:ref) { 'refs/heads/master' }
it 'raises an error' do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'The default branch of a project cannot be deleted.')
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'The default branch of a project cannot be deleted.')
end
end
......@@ -28,7 +28,7 @@ describe Gitlab::Checks::BranchCheck do
it 'raises an error if the user is not allowed to do forced pushes to protected branches' do
expect(Gitlab::Checks::ForcePush).to receive(:force_push?).and_return(true)
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to force push code to a protected branch on this project.')
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You are not allowed to force push code to a protected branch on this project.')
end
it 'raises an error if the user is not allowed to merge to protected branches' do
......@@ -38,13 +38,13 @@ describe Gitlab::Checks::BranchCheck do
expect(user_access).to receive(:can_merge_to_branch?).and_return(false)
expect(user_access).to receive(:can_push_to_branch?).and_return(false)
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to merge code into protected branches on this project.')
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You are not allowed to merge code into protected branches on this project.')
end
it 'raises an error if the user is not allowed to push to protected branches' do
expect(user_access).to receive(:can_push_to_branch?).and_return(false)
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to protected branches on this project.')
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You are not allowed to push code to protected branches on this project.')
end
context 'when project repository is empty' do
......@@ -58,7 +58,7 @@ describe Gitlab::Checks::BranchCheck do
end
it 'raises an error' do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /Ask a project Owner or Maintainer to create a default branch/)
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, /Ask a project Owner or Maintainer to create a default branch/)
end
end
......@@ -109,7 +109,7 @@ describe Gitlab::Checks::BranchCheck do
end
it 'raises an error' do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to create protected branches on this project.')
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You are not allowed to create protected branches on this project.')
end
end
......@@ -135,7 +135,7 @@ describe Gitlab::Checks::BranchCheck do
end
it 'raises an error' do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only use an existing protected branch ref as the basis of a new protected branch.')
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You can only use an existing protected branch ref as the basis of a new protected branch.')
end
end
......@@ -157,7 +157,7 @@ describe Gitlab::Checks::BranchCheck do
context 'via SSH' do
it 'raises an error' do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only create protected branches using the web interface and API.')
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You can only create protected branches using the web interface and API.')
end
end
end
......@@ -171,7 +171,7 @@ describe Gitlab::Checks::BranchCheck do
context 'if the user is not allowed to delete protected branches' do
it 'raises an error' do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to delete protected branches from this project. Only a project maintainer or owner can delete a protected branch.')
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You are not allowed to delete protected branches from this project. Only a project maintainer or owner can delete a protected branch.')
end
end
......@@ -190,7 +190,7 @@ describe Gitlab::Checks::BranchCheck do
context 'over SSH or HTTP' do
it 'raises an error' do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only delete protected branches using the web interface.')
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You can only delete protected branches using the web interface.')
end
end
end
......
......@@ -34,7 +34,7 @@ describe Gitlab::Checks::DiffCheck do
context 'when change is sent by a different user' do
it 'raises an error if the user is not allowed to update the file' do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "The path 'README' is locked in Git LFS by #{lock.user.name}")
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "The path 'README' is locked in Git LFS by #{lock.user.name}")
end
end
......
......@@ -50,7 +50,7 @@ describe Gitlab::Checks::LfsCheck do
end
it 'fails if any LFS blobs are missing' do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /LFS objects are missing/)
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, /LFS objects are missing/)
end
it 'succeeds if LFS objects have already been uploaded' do
......
......@@ -15,7 +15,7 @@ describe Gitlab::Checks::PushCheck do
expect(user_access).to receive(:can_do_action?).with(:push_code).and_return(false)
expect(project).to receive(:branch_allows_collaboration?).with(user_access.user, 'master').and_return(false)
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to this project.')
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You are not allowed to push code to this project.')
end
end
end
......
......@@ -11,7 +11,7 @@ describe Gitlab::Checks::TagCheck do
it 'raises an error when user does not have access' do
allow(user_access).to receive(:can_do_action?).with(:admin_tag).and_return(false)
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to change existing tags on this project.')
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You are not allowed to change existing tags on this project.')
end
context 'with protected tag' do
......@@ -27,7 +27,7 @@ describe Gitlab::Checks::TagCheck do
let(:newrev) { '0000000000000000000000000000000000000000' }
it 'is prevented' do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /cannot be deleted/)
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, /cannot be deleted/)
end
end
......@@ -36,7 +36,7 @@ describe Gitlab::Checks::TagCheck do
let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' }
it 'is prevented' do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /cannot be updated/)
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, /cannot be updated/)
end
end
end
......@@ -47,7 +47,7 @@ describe Gitlab::Checks::TagCheck do
let(:ref) { 'refs/tags/v9.1.0' }
it 'prevents creation below access level' do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /allowed to create this tag as it is protected/)
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, /allowed to create this tag as it is protected/)
end
context 'when user has access' do
......
......@@ -32,8 +32,8 @@ describe Gitlab::GitAccess do
it 'blocks ssh git push and pull' do
aggregate_failures do
expect { push_access_check }.to raise_unauthorized('Git access over SSH is not allowed')
expect { pull_access_check }.to raise_unauthorized('Git access over SSH is not allowed')
expect { push_access_check }.to raise_forbidden('Git access over SSH is not allowed')
expect { pull_access_check }.to raise_forbidden('Git access over SSH is not allowed')
end
end
end
......@@ -48,8 +48,8 @@ describe Gitlab::GitAccess do
it 'blocks http push and pull' do
aggregate_failures do
expect { push_access_check }.to raise_unauthorized('Git access over HTTP is not allowed')
expect { pull_access_check }.to raise_unauthorized('Git access over HTTP is not allowed')
expect { push_access_check }.to raise_forbidden('Git access over HTTP is not allowed')
expect { pull_access_check }.to raise_forbidden('Git access over HTTP is not allowed')
end
end
......@@ -58,7 +58,7 @@ describe Gitlab::GitAccess do
it "doesn't block http pull" do
aggregate_failures do
expect { pull_access_check }.not_to raise_unauthorized('Git access over HTTP is not allowed')
expect { pull_access_check }.not_to raise_forbidden('Git access over HTTP is not allowed')
end
end
......@@ -67,7 +67,7 @@ describe Gitlab::GitAccess do
it "doesn't block http pull" do
aggregate_failures do
expect { pull_access_check }.not_to raise_unauthorized('Git access over HTTP is not allowed')
expect { pull_access_check }.not_to raise_forbidden('Git access over HTTP is not allowed')
end
end
end
......@@ -165,7 +165,7 @@ describe Gitlab::GitAccess do
end
it 'does not block pushes with "not found"' do
expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload])
expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:auth_upload])
end
end
......@@ -178,7 +178,7 @@ describe Gitlab::GitAccess do
end
it 'blocks the push' do
expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:upload])
expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:upload])
end
end
......@@ -208,7 +208,7 @@ describe Gitlab::GitAccess do
end
it 'does not block pushes with "not found"' do
expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:upload])
expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:upload])
end
end
......@@ -285,8 +285,8 @@ describe Gitlab::GitAccess do
it 'does not allow keys which are too small', :aggregate_failures do
expect(actor).not_to be_valid
expect { pull_access_check }.to raise_unauthorized('Your SSH key must be at least 4096 bits.')
expect { push_access_check }.to raise_unauthorized('Your SSH key must be at least 4096 bits.')
expect { pull_access_check }.to raise_forbidden('Your SSH key must be at least 4096 bits.')
expect { push_access_check }.to raise_forbidden('Your SSH key must be at least 4096 bits.')
end
end
......@@ -297,8 +297,8 @@ describe Gitlab::GitAccess do
it 'does not allow keys which are too small', :aggregate_failures do
expect(actor).not_to be_valid
expect { pull_access_check }.to raise_unauthorized(/Your SSH key type is forbidden/)
expect { push_access_check }.to raise_unauthorized(/Your SSH key type is forbidden/)
expect { pull_access_check }.to raise_forbidden(/Your SSH key type is forbidden/)
expect { push_access_check }.to raise_forbidden(/Your SSH key type is forbidden/)
end
end
end
......@@ -363,7 +363,7 @@ describe Gitlab::GitAccess do
let(:authentication_abilities) { [] }
it 'raises unauthorized with download error' do
expect { pull_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_download])
expect { pull_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:auth_download])
end
context 'when authentication abilities include download code' do
......@@ -387,7 +387,7 @@ describe Gitlab::GitAccess do
let(:authentication_abilities) { [] }
it 'raises unauthorized with push error' do
expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload])
expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:auth_upload])
end
context 'when authentication abilities include push code' do
......@@ -414,7 +414,7 @@ describe Gitlab::GitAccess do
end
context 'when calling git-upload-pack' do
it { expect { pull_access_check }.to raise_unauthorized('Pulling over HTTP is not allowed.') }
it { expect { pull_access_check }.to raise_forbidden('Pulling over HTTP is not allowed.') }
end
context 'when calling git-receive-pack' do
......@@ -428,7 +428,7 @@ describe Gitlab::GitAccess do
end
context 'when calling git-receive-pack' do
it { expect { push_access_check }.to raise_unauthorized('Pushing over HTTP is not allowed.') }
it { expect { push_access_check }.to raise_forbidden('Pushing over HTTP is not allowed.') }
end
context 'when calling git-upload-pack' do
......@@ -445,7 +445,7 @@ describe Gitlab::GitAccess do
allow(Gitlab::Database).to receive(:read_only?) { true }
end
it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:cannot_push_to_read_only]) }
it { expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:cannot_push_to_read_only]) }
end
end
......@@ -559,21 +559,21 @@ describe Gitlab::GitAccess do
it 'disallows guests to pull' do
project.add_guest(user)
expect { pull_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:download])
expect { pull_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:download])
end
it 'disallows blocked users to pull' do
project.add_maintainer(user)
user.block
expect { pull_access_check }.to raise_unauthorized('Your account has been blocked.')
expect { pull_access_check }.to raise_forbidden('Your account has been blocked.')
end
it 'disallows deactivated users to pull' do
project.add_maintainer(user)
user.deactivate!
expect { pull_access_check }.to raise_unauthorized("Your account has been deactivated by your administrator. Please log back in from a web browser to reactivate your account at #{Gitlab.config.gitlab.url}")
expect { pull_access_check }.to raise_forbidden("Your account has been deactivated by your administrator. Please log back in from a web browser to reactivate your account at #{Gitlab.config.gitlab.url}")
end
context 'when the project repository does not exist' do
......@@ -610,7 +610,7 @@ describe Gitlab::GitAccess do
it 'does not give access to download code' do
public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED)
expect { pull_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:download])
expect { pull_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:download])
end
end
end
......@@ -722,7 +722,7 @@ describe Gitlab::GitAccess do
context 'when is not member of the project' do
context 'pull code' do
it { expect { pull_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:download]) }
it { expect { pull_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:download]) }
end
end
end
......@@ -828,7 +828,7 @@ describe Gitlab::GitAccess do
expect(&check).not_to raise_error,
-> { "expected #{action} to be allowed" }
else
expect(&check).to raise_error(Gitlab::GitAccess::UnauthorizedError),
expect(&check).to raise_error(Gitlab::GitAccess::ForbiddenError),
-> { "expected #{action} to be disallowed" }
end
end
......@@ -965,7 +965,7 @@ describe Gitlab::GitAccess do
it 'does not allow deactivated users to push' do
user.deactivate!
expect { push_access_check }.to raise_unauthorized("Your account has been deactivated by your administrator. Please log back in from a web browser to reactivate your account at #{Gitlab.config.gitlab.url}")
expect { push_access_check }.to raise_forbidden("Your account has been deactivated by your administrator. Please log back in from a web browser to reactivate your account at #{Gitlab.config.gitlab.url}")
end
it 'cleans up the files' do
......@@ -1009,26 +1009,26 @@ describe Gitlab::GitAccess do
project.add_reporter(user)
end
it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload]) }
it { expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:auth_upload]) }
end
context 'when unauthorized' do
context 'to public project' do
let(:project) { create(:project, :public, :repository) }
it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload]) }
it { expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:auth_upload]) }
end
context 'to internal project' do
let(:project) { create(:project, :internal, :repository) }
it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload]) }
it { expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:auth_upload]) }
end
context 'to private project' do
let(:project) { create(:project, :private, :repository) }
it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload]) }
it { expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:auth_upload]) }
end
end
end
......@@ -1039,7 +1039,7 @@ describe Gitlab::GitAccess do
it 'denies push access' do
project.add_maintainer(user)
expect { push_access_check }.to raise_unauthorized('The repository is temporarily read-only. Please try again later.')
expect { push_access_check }.to raise_forbidden('The repository is temporarily read-only. Please try again later.')
end
end
......@@ -1060,7 +1060,7 @@ describe Gitlab::GitAccess do
context 'to public project' do
let(:project) { create(:project, :public, :repository) }
it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:deploy_key_upload]) }
it { expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:deploy_key_upload]) }
end
context 'to internal project' do
......@@ -1083,14 +1083,14 @@ describe Gitlab::GitAccess do
key.deploy_keys_projects.create(project: project, can_push: false)
end
it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:deploy_key_upload]) }
it { expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:deploy_key_upload]) }
end
context 'when unauthorized' do
context 'to public project' do
let(:project) { create(:project, :public, :repository) }
it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:deploy_key_upload]) }
it { expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:deploy_key_upload]) }
end
context 'to internal project' do
......@@ -1121,7 +1121,7 @@ describe Gitlab::GitAccess do
it 'blocks access when the user did not accept terms', :aggregate_failures do
actions.each do |action|
expect { action.call }.to raise_unauthorized(/must accept the Terms of Service in order to perform this action/)
expect { action.call }.to raise_forbidden(/must accept the Terms of Service in order to perform this action/)
end
end
......@@ -1211,8 +1211,8 @@ describe Gitlab::GitAccess do
access.check('git-receive-pack', changes)
end
def raise_unauthorized(message)
raise_error(Gitlab::GitAccess::UnauthorizedError, message)
def raise_forbidden(message)
raise_error(Gitlab::GitAccess::ForbiddenError, message)
end
def raise_not_found
......
......@@ -33,7 +33,7 @@ describe Gitlab::GitAccessWiki do
end
it 'does not give access to upload wiki code' do
expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "You can't push code to a read-only GitLab instance.")
expect { subject }.to raise_error(Gitlab::GitAccess::ForbiddenError, "You can't push code to a read-only GitLab instance.")
end
end
end
......@@ -70,7 +70,7 @@ describe Gitlab::GitAccessWiki do
it 'does not give access to download wiki code' do
project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED)
expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to download code from this project.')
expect { subject }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You are not allowed to download code from this project.')
end
end
end
......
......@@ -306,5 +306,19 @@ describe Gitlab::GitalyClient::CommitService do
client.find_commits(order: 'topo')
end
it 'sends an RPC request with an author' do
request = Gitaly::FindCommitsRequest.new(
repository: repository_message,
disable_walk: true,
order: 'NONE',
author: "Billy Baggins <bilbo@shire.com>"
)
expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:find_commits)
.with(request, kind_of(Hash)).and_return([])
client.find_commits(order: 'default', author: "Billy Baggins <bilbo@shire.com>")
end
end
end
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::ImportExport::BaseObjectBuilder do
describe Gitlab::ImportExport::Base::ObjectBuilder do
let(:project) do
create(:project, :repository,
:builds_disabled,
......@@ -11,7 +11,7 @@ describe Gitlab::ImportExport::BaseObjectBuilder do
path: 'project')
end
let(:klass) { Milestone }
let(:attributes) { { 'title' => 'Test BaseObjectBuilder Milestone', 'project' => project } }
let(:attributes) { { 'title' => 'Test Base::ObjectBuilder Milestone', 'project' => project } }
subject { described_class.build(klass, attributes) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::ImportExport::BaseRelationFactory do
describe Gitlab::ImportExport::Base::RelationFactory do
let(:user) { create(:admin) }
let(:project) { create(:project) }
let(:members_mapper) { double('members_mapper').as_null_object }
......@@ -13,7 +13,7 @@ describe Gitlab::ImportExport::BaseRelationFactory do
subject do
described_class.create(relation_sym: relation_sym,
relation_hash: relation_hash,
object_builder: Gitlab::ImportExport::GroupProjectObjectBuilder,
object_builder: Gitlab::ImportExport::Project::ObjectBuilder,
members_mapper: members_mapper,
user: user,
importable: project,
......
......@@ -24,11 +24,11 @@ describe 'forked project import' do
end
let(:saver) do
Gitlab::ImportExport::ProjectTreeSaver.new(project: project_with_repo, current_user: user, shared: shared)
Gitlab::ImportExport::Project::TreeSaver.new(project: project_with_repo, current_user: user, shared: shared)
end
let(:restorer) do
Gitlab::ImportExport::ProjectTreeRestorer.new(user: user, shared: shared, project: project)
Gitlab::ImportExport::Project::TreeRestorer.new(user: user, shared: shared, project: project)
end
before do
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::ImportExport::GroupObjectBuilder do
describe Gitlab::ImportExport::Group::ObjectBuilder do
let(:group) { create(:group) }
let(:base_attributes) do
{
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::ImportExport::GroupRelationFactory do
describe Gitlab::ImportExport::Group::RelationFactory do
let(:group) { create(:group) }
let(:members_mapper) { double('members_mapper').as_null_object }
let(:user) { create(:admin) }
......@@ -11,7 +11,7 @@ describe Gitlab::ImportExport::GroupRelationFactory do
described_class.create(relation_sym: relation_sym,
relation_hash: relation_hash,
members_mapper: members_mapper,
object_builder: Gitlab::ImportExport::GroupObjectBuilder,
object_builder: Gitlab::ImportExport::Group::ObjectBuilder,
user: user,
importable: group,
excluded_keys: excluded_keys)
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::ImportExport::GroupTreeRestorer do
describe Gitlab::ImportExport::Group::TreeRestorer do
include ImportExport::CommonUtil
let(:shared) { Gitlab::ImportExport::Shared.new(group) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::ImportExport::GroupTreeSaver do
describe Gitlab::ImportExport::Group::TreeSaver do
describe 'saves the group tree into a json object' do
let(:shared) { Gitlab::ImportExport::Shared.new(group) }
let(:group_tree_saver) { described_class.new(group: group, current_user: user, shared: shared) }
......@@ -72,7 +72,7 @@ describe Gitlab::ImportExport::GroupTreeSaver do
# except:
# context 'with description override' do
# context 'group members' do
# ^ These are specific for the groupTreeSaver
# ^ These are specific for the Group::TreeSaver
context 'JSON' do
let(:saved_group_json) do
group_tree_saver.save
......
......@@ -63,7 +63,7 @@ describe Gitlab::ImportExport::Importer do
end
it 'restores the ProjectTree' do
expect(Gitlab::ImportExport::ProjectTreeRestorer).to receive(:new).and_call_original
expect(Gitlab::ImportExport::Project::TreeRestorer).to receive(:new).and_call_original
importer.execute
end
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::ImportExport::GroupProjectObjectBuilder do
describe Gitlab::ImportExport::Project::ObjectBuilder do
let!(:group) { create(:group, :private) }
let!(:subgroup) { create(:group, :private, parent: group) }
let!(:project) do
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::ImportExport::ProjectRelationFactory do
describe Gitlab::ImportExport::Project::RelationFactory do
let(:group) { create(:group) }
let(:project) { create(:project, :repository, group: group) }
let(:members_mapper) { double('members_mapper').as_null_object }
......@@ -11,7 +11,7 @@ describe Gitlab::ImportExport::ProjectRelationFactory do
let(:created_object) do
described_class.create(relation_sym: relation_sym,
relation_hash: relation_hash,
object_builder: Gitlab::ImportExport::GroupProjectObjectBuilder,
object_builder: Gitlab::ImportExport::Project::ObjectBuilder,
members_mapper: members_mapper,
user: user,
importable: project,
......@@ -243,11 +243,11 @@ describe Gitlab::ImportExport::ProjectRelationFactory do
context 'Project references' do
let(:relation_sym) { :project_foo_model }
let(:relation_hash) do
Gitlab::ImportExport::ProjectRelationFactory::PROJECT_REFERENCES.map { |ref| { ref => 99 } }.inject(:merge)
Gitlab::ImportExport::Project::RelationFactory::PROJECT_REFERENCES.map { |ref| { ref => 99 } }.inject(:merge)
end
class ProjectFooModel < FooModel
attr_accessor(*Gitlab::ImportExport::ProjectRelationFactory::PROJECT_REFERENCES)
attr_accessor(*Gitlab::ImportExport::Project::RelationFactory::PROJECT_REFERENCES)
end
before do
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::ImportExport::ProjectTreeLoader do
describe Gitlab::ImportExport::Project::TreeLoader do
let(:fixture) { 'spec/fixtures/lib/gitlab/import_export/with_duplicates.json' }
let(:project_tree) { JSON.parse(File.read(fixture)) }
......
# frozen_string_literal: true
require 'spec_helper'
include ImportExport::CommonUtil
describe Gitlab::ImportExport::ProjectTreeRestorer do
describe Gitlab::ImportExport::Project::TreeRestorer do
include ImportExport::CommonUtil
let(:shared) { project.import_export_shared }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::ImportExport::ProjectTreeSaver do
describe Gitlab::ImportExport::Project::TreeSaver do
describe 'saves the project tree into a json object' do
let(:shared) { project.import_export_shared }
let(:project_tree_saver) { described_class.new(project: project, current_user: user, shared: shared) }
......@@ -75,7 +75,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
# except:
# context 'with description override' do
# context 'group members' do
# ^ These are specific for the ProjectTreeSaver
# ^ These are specific for the Project::TreeSaver
context 'JSON' do
let(:saved_project_json) do
project_tree_saver.save
......
......@@ -22,7 +22,7 @@ describe Gitlab::ImportExport::RelationRenameService do
end
context 'when importing' do
let(:project_tree_restorer) { Gitlab::ImportExport::ProjectTreeRestorer.new(user: user, shared: shared, project: project) }
let(:project_tree_restorer) { Gitlab::ImportExport::Project::TreeRestorer.new(user: user, shared: shared, project: project) }
let(:file_content) { IO.read(File.join(shared.export_path, 'project.json')) }
let(:json_file) { ActiveSupport::JSON.decode(file_content) }
......@@ -99,7 +99,7 @@ describe Gitlab::ImportExport::RelationRenameService do
let(:relation_tree_saver) { Gitlab::ImportExport::RelationTreeSaver.new }
let(:project_tree_saver) do
Gitlab::ImportExport::ProjectTreeSaver.new(
Gitlab::ImportExport::Project::TreeSaver.new(
project: project, current_user: user, shared: shared)
end
......
......@@ -39,8 +39,8 @@ describe Gitlab::ImportExport::RelationTreeRestorer do
context 'when restoring a project' do
let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/project.json' }
let(:importable) { create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') }
let(:object_builder) { Gitlab::ImportExport::GroupProjectObjectBuilder }
let(:relation_factory) { Gitlab::ImportExport::ProjectRelationFactory }
let(:object_builder) { Gitlab::ImportExport::Project::ObjectBuilder }
let(:relation_factory) { Gitlab::ImportExport::Project::RelationFactory }
let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) }
let(:tree_hash) { importable_hash }
......
......@@ -9,7 +9,7 @@ describe GitlabDanger do
describe '.local_warning_message' do
it 'returns an informational message with rules that can run' do
expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changes_size, gemfile, documentation, frozen_string, duplicate_yarn_dependencies, prettier, eslint, database, commit_messages')
expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changes_size, documentation, frozen_string, duplicate_yarn_dependencies, prettier, eslint, database, commit_messages')
end
end
......
......@@ -320,6 +320,21 @@ describe Repository do
end
end
context "when 'author' is set" do
it "returns commits from that author" do
commit = repository.commits(nil, limit: 1).first
known_author = "#{commit.author_name} <#{commit.author_email}>"
expect(repository.commits(nil, author: known_author, limit: 1)).not_to be_empty
end
it "doesn't returns commits from an unknown author" do
unknown_author = "The Man With No Name <zapp@brannigan.com>"
expect(repository.commits(nil, author: unknown_author, limit: 1)).to be_empty
end
end
context "when 'all' flag is set" do
it 'returns every commit from the repository' do
expect(repository.commits(nil, all: true, limit: 60).size).to eq(60)
......
......@@ -409,7 +409,7 @@ describe API::Internal::Base do
it do
pull(key, project)
expect(response).to have_gitlab_http_status(:unauthorized)
expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response["status"]).to be_falsey
expect(user.reload.last_activity_on).to be_nil
end
......@@ -419,7 +419,7 @@ describe API::Internal::Base do
it do
push(key, project)
expect(response).to have_gitlab_http_status(:unauthorized)
expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response["status"]).to be_falsey
expect(user.reload.last_activity_on).to be_nil
end
......@@ -518,7 +518,7 @@ describe API::Internal::Base do
it do
pull(key, personal_project)
expect(response).to have_gitlab_http_status(:unauthorized)
expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response["status"]).to be_falsey
expect(user.reload.last_activity_on).to be_nil
end
......@@ -528,7 +528,7 @@ describe API::Internal::Base do
it do
push(key, personal_project)
expect(response).to have_gitlab_http_status(:unauthorized)
expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response["status"]).to be_falsey
expect(user.reload.last_activity_on).to be_nil
end
......@@ -572,7 +572,7 @@ describe API::Internal::Base do
it do
push(key, project)
expect(response).to have_gitlab_http_status(:unauthorized)
expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response["status"]).to be_falsey
end
end
......@@ -654,7 +654,7 @@ describe API::Internal::Base do
it 'rejects the SSH push' do
push(key, project)
expect(response.status).to eq(401)
expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response['status']).to be_falsey
expect(json_response['message']).to eq 'Git access over SSH is not allowed'
end
......@@ -662,7 +662,7 @@ describe API::Internal::Base do
it 'rejects the SSH pull' do
pull(key, project)
expect(response.status).to eq(401)
expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response['status']).to be_falsey
expect(json_response['message']).to eq 'Git access over SSH is not allowed'
end
......@@ -676,7 +676,7 @@ describe API::Internal::Base do
it 'rejects the HTTP push' do
push(key, project, 'http')
expect(response.status).to eq(401)
expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response['status']).to be_falsey
expect(json_response['message']).to eq 'Git access over HTTP is not allowed'
end
......@@ -684,7 +684,7 @@ describe API::Internal::Base do
it 'rejects the HTTP pull' do
pull(key, project, 'http')
expect(response.status).to eq(401)
expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response['status']).to be_falsey
expect(json_response['message']).to eq 'Git access over HTTP is not allowed'
end
......
......@@ -7,7 +7,7 @@ describe Groups::ImportExport::ExportService do
let!(:user) { create(:user) }
let(:group) { create(:group) }
let(:shared) { Gitlab::ImportExport::Shared.new(group) }
let(:export_path) { shared.export_path }
let(:archive_path) { shared.archive_path }
let(:service) { described_class.new(group: group, user: user, params: { shared: shared }) }
before do
......@@ -15,11 +15,11 @@ describe Groups::ImportExport::ExportService do
end
after do
FileUtils.rm_rf(export_path)
FileUtils.rm_rf(archive_path)
end
it 'saves the models' do
expect(Gitlab::ImportExport::GroupTreeSaver).to receive(:new).and_call_original
expect(Gitlab::ImportExport::Group::TreeSaver).to receive(:new).and_call_original
service.execute
end
......@@ -29,7 +29,7 @@ describe Groups::ImportExport::ExportService do
service.execute
expect(group.import_export_upload.export_file.file).not_to be_nil
expect(File.directory?(export_path)).to eq(false)
expect(File.directory?(archive_path)).to eq(false)
expect(File.exist?(shared.archive_path)).to eq(false)
end
end
......@@ -46,26 +46,43 @@ describe Groups::ImportExport::ExportService do
end
end
context 'when saving services fail' do
context 'when export fails' do
context 'when file saver fails' do
it 'removes the remaining exported data' do
allow_next_instance_of(Gitlab::ImportExport::Saver) do |saver|
allow(saver).to receive(:save).and_return(false)
end
expect { service.execute }.to raise_error(Gitlab::ImportExport::Error)
expect(group.import_export_upload).to be_nil
expect(File.exist?(shared.archive_path)).to eq(false)
end
end
context 'when file compression fails' do
before do
allow(service).to receive_message_chain(:tree_exporter, :save).and_return(false)
end
it 'removes the remaining exported data' do
allow_any_instance_of(Gitlab::ImportExport::Saver).to receive(:compress_and_save).and_return(false)
allow_next_instance_of(Gitlab::ImportExport::Saver) do |saver|
allow(saver).to receive(:compress_and_save).and_return(false)
end
expect { service.execute }.to raise_error(Gitlab::ImportExport::Error)
expect(group.import_export_upload).to be_nil
expect(File.directory?(export_path)).to eq(false)
expect(File.exist?(shared.archive_path)).to eq(false)
end
it 'notifies logger' do
expect_any_instance_of(Gitlab::Import::Logger).to receive(:error)
allow(service).to receive_message_chain(:tree_exporter, :save).and_return(false)
expect(shared.logger).to receive(:error)
expect { service.execute }.to raise_error(Gitlab::ImportExport::Error)
end
end
end
end
end
......@@ -27,7 +27,7 @@ describe Projects::ImportExport::ExportService do
end
it 'saves the models' do
expect(Gitlab::ImportExport::ProjectTreeSaver).to receive(:new).and_call_original
expect(Gitlab::ImportExport::Project::TreeSaver).to receive(:new).and_call_original
service.execute
end
......@@ -91,10 +91,10 @@ describe Projects::ImportExport::ExportService do
end
it 'removes the remaining exported data' do
allow(shared).to receive(:export_path).and_return('whatever')
allow(shared).to receive(:archive_path).and_return('whatever')
allow(FileUtils).to receive(:rm_rf)
expect(FileUtils).to receive(:rm_rf).with(shared.export_path)
expect(FileUtils).to receive(:rm_rf).with(shared.archive_path)
end
it 'notifies the user' do
......@@ -121,10 +121,10 @@ describe Projects::ImportExport::ExportService do
end
it 'removes the remaining exported data' do
allow(shared).to receive(:export_path).and_return('whatever')
allow(shared).to receive(:archive_path).and_return('whatever')
allow(FileUtils).to receive(:rm_rf)
expect(FileUtils).to receive(:rm_rf).with(shared.export_path)
expect(FileUtils).to receive(:rm_rf).with(shared.archive_path)
end
it 'notifies the user' do
......@@ -142,6 +142,21 @@ describe Projects::ImportExport::ExportService do
end
end
context 'when one of the savers fail unexpectedly' do
let(:archive_path) { shared.archive_path }
before do
allow(service).to receive_message_chain(:uploads_saver, :save).and_return(false)
end
it 'removes the remaining exported data' do
expect { service.execute }.to raise_error(Gitlab::ImportExport::Error)
expect(project.import_export_upload).to be_nil
expect(File.exist?(shared.archive_path)).to eq(false)
end
end
context 'when user does not have admin_project permission' do
let!(:another_user) { create(:user) }
......
......@@ -3,6 +3,11 @@
module WikiHelpers
extend self
def wait_for_svg_to_be_loaded(example = nil)
# Ensure the SVG is loaded first before clicking the button
find('.svg-content .js-lazy-loaded') if example.nil? || example.metadata.key?(:js)
end
def upload_file_to_wiki(project, user, file_name)
opts = {
file_name: file_name,
......
......@@ -34,13 +34,13 @@ module ImportExport
end
def get_project_restorer(project, import_path)
Gitlab::ImportExport::ProjectTreeRestorer.new(
Gitlab::ImportExport::Project::TreeRestorer.new(
user: project.creator, shared: get_shared_env(path: import_path), project: project
)
end
def get_project_saver(project, export_path)
Gitlab::ImportExport::ProjectTreeSaver.new(
Gitlab::ImportExport::Project::TreeSaver.new(
project: project, current_user: project.creator, shared: get_shared_env(path: export_path)
)
end
......
......@@ -36,8 +36,8 @@ module ConfigurationHelper
end
def relation_class_for_name(relation_name)
relation_name = Gitlab::ImportExport::ProjectRelationFactory.overrides[relation_name.to_sym] || relation_name
Gitlab::ImportExport::ProjectRelationFactory.relation_class(relation_name)
relation_name = Gitlab::ImportExport::Project::RelationFactory.overrides[relation_name.to_sym] || relation_name
Gitlab::ImportExport::Project::RelationFactory.relation_class(relation_name)
end
def parsed_attributes(relation_name, attributes, config: Gitlab::ImportExport.config_file)
......
......@@ -42,7 +42,7 @@ RSpec.shared_examples 'wiki file attachments' do
end
end
context 'uploading is complete', :quarantine do
context 'uploading is complete' do
it 'shows "Attach a file" button on uploading complete' do
attach_with_dropzone
wait_for_requests
......
# frozen_string_literal: true
# Shared examples for ProjectTreeRestorer (shared to allow the testing
# Shared examples for Project::TreeRestorer (shared to allow the testing
# of EE-specific features)
RSpec.shared_examples 'restores project successfully' do |**results|
it 'restores the project' do
......
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