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 ...@@ -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? 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 @limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i
search = params[:search] search = params[:search]
author = params[:author]
@commits = @commits =
if search.present? if search.present?
@repository.find_commits_by_message(search, @ref, @path, @limit, @offset) @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 else
@repository.commits(@ref, path: @path, limit: @limit, offset: @offset) @repository.commits(@ref, path: @path, limit: @limit, offset: @offset)
end end
......
...@@ -7,7 +7,7 @@ module Repositories ...@@ -7,7 +7,7 @@ module Repositories
before_action :access_check before_action :access_check
prepend_before_action :deny_head_requests, only: [:info_refs] 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::NotFoundError, with: :render_404_with_exception
rescue_from Gitlab::GitAccess::ProjectCreationError, with: :render_422_with_exception rescue_from Gitlab::GitAccess::ProjectCreationError, with: :render_422_with_exception
rescue_from Gitlab::GitAccess::TimeoutError, with: :render_503_with_exception rescue_from Gitlab::GitAccess::TimeoutError, with: :render_503_with_exception
......
...@@ -599,7 +599,7 @@ module Ci ...@@ -599,7 +599,7 @@ module Ci
# Manually set the notes for a Ci::Pipeline # Manually set the notes for a Ci::Pipeline
# There is no ActiveRecord relation between Ci::Pipeline and notes # There is no ActiveRecord relation between Ci::Pipeline and notes
# as they are related to a commit sha. This method helps importing # 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) def notes=(notes)
notes.each do |note| notes.each do |note|
note[:id] = nil note[:id] = nil
......
...@@ -139,6 +139,7 @@ class Repository ...@@ -139,6 +139,7 @@ class Repository
repo: raw_repository, repo: raw_repository,
ref: ref, ref: ref,
path: opts[:path], path: opts[:path],
author: opts[:author],
follow: Array(opts[:path]).length == 1, follow: Array(opts[:path]).length == 1,
limit: opts[:limit], limit: opts[:limit],
offset: opts[:offset], offset: opts[:offset],
......
...@@ -18,6 +18,8 @@ module Groups ...@@ -18,6 +18,8 @@ module Groups
end end
save! save!
ensure
cleanup
end end
private private
...@@ -28,7 +30,7 @@ module Groups ...@@ -28,7 +30,7 @@ module Groups
if savers.all?(&:save) if savers.all?(&:save)
notify_success notify_success
else else
cleanup_and_notify_error! notify_error!
end end
end end
...@@ -37,21 +39,19 @@ module Groups ...@@ -37,21 +39,19 @@ module Groups
end end
def tree_exporter 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 end
def file_saver def file_saver
Gitlab::ImportExport::Saver.new(exportable: @group, shared: @shared) Gitlab::ImportExport::Saver.new(exportable: @group, shared: @shared)
end end
def cleanup_and_notify_error def cleanup
FileUtils.rm_rf(shared.export_path) FileUtils.rm_rf(shared.archive_path) if shared&.archive_path
notify_error
end end
def cleanup_and_notify_error! def notify_error!
cleanup_and_notify_error notify_error
raise Gitlab::ImportExport::Error.new(shared.errors.to_sentence) raise Gitlab::ImportExport::Error.new(shared.errors.to_sentence)
end end
......
...@@ -34,7 +34,7 @@ module Groups ...@@ -34,7 +34,7 @@ module Groups
end end
def restorer def restorer
@restorer ||= Gitlab::ImportExport::GroupTreeRestorer.new(user: @current_user, @restorer ||= Gitlab::ImportExport::Group::TreeRestorer.new(user: @current_user,
shared: @shared, shared: @shared,
group: @group, group: @group,
group_hash: nil) group_hash: nil)
......
...@@ -4,13 +4,13 @@ module Lfs ...@@ -4,13 +4,13 @@ module Lfs
class LockFileService < BaseService class LockFileService < BaseService
def execute def execute
unless can?(current_user, :push_code, project) unless can?(current_user, :push_code, project)
raise Gitlab::GitAccess::UnauthorizedError, 'You have no permissions' raise Gitlab::GitAccess::ForbiddenError, 'You have no permissions'
end end
create_lock! create_lock!
rescue ActiveRecord::RecordNotUnique rescue ActiveRecord::RecordNotUnique
error('already locked', 409, current_lock) error('already locked', 409, current_lock)
rescue Gitlab::GitAccess::UnauthorizedError => ex rescue Gitlab::GitAccess::ForbiddenError => ex
error(ex.message, 403) error(ex.message, 403)
rescue => ex rescue => ex
error(ex.message, 500) error(ex.message, 500)
......
...@@ -4,11 +4,11 @@ module Lfs ...@@ -4,11 +4,11 @@ module Lfs
class UnlockFileService < BaseService class UnlockFileService < BaseService
def execute def execute
unless can?(current_user, :push_code, project) unless can?(current_user, :push_code, project)
raise Gitlab::GitAccess::UnauthorizedError, _('You have no permissions') raise Gitlab::GitAccess::ForbiddenError, _('You have no permissions')
end end
unlock_file unlock_file
rescue Gitlab::GitAccess::UnauthorizedError => ex rescue Gitlab::GitAccess::ForbiddenError => ex
error(ex.message, 403) error(ex.message, 403)
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
error(_('Lock not found'), 404) error(_('Lock not found'), 404)
......
...@@ -14,6 +14,8 @@ module Projects ...@@ -14,6 +14,8 @@ module Projects
save_all! save_all!
execute_after_export_action(after_export_strategy) execute_after_export_action(after_export_strategy)
ensure
cleanup
end end
private private
...@@ -24,7 +26,7 @@ module Projects ...@@ -24,7 +26,7 @@ module Projects
return unless after_export_strategy return unless after_export_strategy
unless after_export_strategy.execute(current_user, project) unless after_export_strategy.execute(current_user, project)
cleanup_and_notify_error notify_error
end end
end end
...@@ -33,7 +35,7 @@ module Projects ...@@ -33,7 +35,7 @@ module Projects
Gitlab::ImportExport::Saver.save(exportable: project, shared: shared) Gitlab::ImportExport::Saver.save(exportable: project, shared: shared)
notify_success notify_success
else else
cleanup_and_notify_error! notify_error!
end end
end end
...@@ -54,7 +56,7 @@ module Projects ...@@ -54,7 +56,7 @@ module Projects
end end
def project_tree_saver 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 end
def uploads_saver def uploads_saver
...@@ -73,16 +75,12 @@ module Projects ...@@ -73,16 +75,12 @@ module Projects
Gitlab::ImportExport::LfsSaver.new(project: project, shared: shared) Gitlab::ImportExport::LfsSaver.new(project: project, shared: shared)
end end
def cleanup_and_notify_error def cleanup
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.archive_path) if shared&.archive_path
FileUtils.rm_rf(shared.export_path)
notify_error
end end
def cleanup_and_notify_error! def notify_error!
cleanup_and_notify_error notify_error
raise Gitlab::ImportExport::Error.new(shared.errors.to_sentence) raise Gitlab::ImportExport::Error.new(shared.errors.to_sentence)
end end
...@@ -92,6 +90,8 @@ module Projects ...@@ -92,6 +90,8 @@ module Projects
end end
def notify_error 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) notification_service.project_not_exported(project, current_user, shared.errors)
end end
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 ...@@ -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+|| |**[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+|| |**[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|| |**[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: ...@@ -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) 1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure)
for the changes to take effect. for the changes to take effect.
......
...@@ -81,7 +81,7 @@ The last option is to import a project using a Rails console: ...@@ -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 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 ```ruby
shared_class = Struct.new(:export_path) do shared_class = Struct.new(:export_path) do
...@@ -98,7 +98,7 @@ The last option is to import a project using a Rails console: ...@@ -98,7 +98,7 @@ The last option is to import a project using a Rails console:
begin begin
#Enable Request store #Enable Request store
RequestStore.begin! 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 ensure
RequestStore.end! RequestStore.end!
RequestStore.clear! RequestStore.clear!
...@@ -128,11 +128,11 @@ The last option is to import a project using a Rails console: ...@@ -128,11 +128,11 @@ The last option is to import a project using a Rails console:
For Performance testing, we should: 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. - 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. - Count the number of executed SQL queries during the restore.
- Observe the number of GC cycles happening. - 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: 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. ...@@ -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. > [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 and can block malicious traffic before it reaches your application. The benefits
of a WAF are: of a WAF are:
...@@ -266,7 +266,7 @@ of a WAF are: ...@@ -266,7 +266,7 @@ of a WAF are:
- Access control for your application - Access control for your application
- Highly configurable logging and blocking rules - 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, 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/), 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). ...@@ -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 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). [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 There is a small performance overhead by enabling ModSecurity. If this is
considered significant for your application, you can disable ModSecurity's considered significant for your application, you can disable ModSecurity's
rule engine for your deployed application by setting rule engine for your deployed application by setting
...@@ -693,7 +690,7 @@ cilium: ...@@ -693,7 +690,7 @@ cilium:
``` ```
The `clusterType` variable enables the recommended Helm variables for 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 check the recommended variables for each cluster type in the official
documentation: documentation:
...@@ -720,13 +717,13 @@ information. ...@@ -720,13 +717,13 @@ information.
By default, Cilium will drop all non-whitelisted packets upon policy By default, Cilium will drop all non-whitelisted packets upon policy
deployment. The audit mode is scheduled for release in deployment. The audit mode is scheduled for release in
[Cilium 1.8](https://github.com/cilium/cilium/pull/9970). In the audit [Cilium 1.8](https://github.com/cilium/cilium/pull/9970). In the audit
mode non-whitelisted packets will not be dropped, instead audit mode, non-whitelisted packets will not be dropped, and audit
notifications will be generated. GitLab provides alternative Docker notifications will be generated instead. GitLab provides alternative Docker
images for Cilium with the audit patch included. You can switch to the 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 custom build and enable the audit mode by adding the following to
`.gitlab/managed-apps/cilium/values.yaml`: `.gitlab/managed-apps/cilium/values.yaml`:
```yml ```yaml
global: global:
registry: registry.gitlab.com/gitlab-org/defend/cilium registry: registry.gitlab.com/gitlab-org/defend/cilium
policyAuditMode: true policyAuditMode: true
...@@ -737,15 +734,15 @@ agent: ...@@ -737,15 +734,15 @@ agent:
``` ```
The Cilium monitor log for traffic is logged out by the 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 ```shell
kubectl -n gitlab-managed-apps logs cilium-XXXX cilium-monitor 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: agent:
monitor: monitor:
enabled: false enabled: false
......
...@@ -89,7 +89,7 @@ The following items will NOT be exported: ...@@ -89,7 +89,7 @@ The following items will NOT be exported:
NOTE: **Note:** NOTE: **Note:**
For more details on the specific data persisted in a project export, see the 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 ## Exporting a project and its data
......
...@@ -49,8 +49,8 @@ module API ...@@ -49,8 +49,8 @@ module API
result = access_checker.check(params[:action], params[:changes]) result = access_checker.check(params[:action], params[:changes])
@project ||= access_checker.project @project ||= access_checker.project
result result
rescue Gitlab::GitAccess::UnauthorizedError => e rescue Gitlab::GitAccess::ForbiddenError => e
return response_with_status(code: 401, success: false, message: e.message) return response_with_status(code: 403, success: false, message: e.message)
rescue Gitlab::GitAccess::TimeoutError => e rescue Gitlab::GitAccess::TimeoutError => e
return response_with_status(code: 503, success: false, message: e.message) return response_with_status(code: 503, success: false, message: e.message)
rescue Gitlab::GitAccess::NotFoundError => e rescue Gitlab::GitAccess::NotFoundError => e
......
...@@ -28,7 +28,7 @@ module Gitlab ...@@ -28,7 +28,7 @@ module Gitlab
logger.log_timed(LOG_MESSAGES[:delete_default_branch_check]) do logger.log_timed(LOG_MESSAGES[:delete_default_branch_check]) do
if deletion? && branch_name == project.default_branch 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
end end
...@@ -42,7 +42,7 @@ module Gitlab ...@@ -42,7 +42,7 @@ module Gitlab
return unless ProtectedBranch.protected?(project, branch_name) # rubocop:disable Cop/AvoidReturnFromBlocks return unless ProtectedBranch.protected?(project, branch_name) # rubocop:disable Cop/AvoidReturnFromBlocks
if forced_push? if forced_push?
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:force_push_protected_branch] raise GitAccess::ForbiddenError, ERROR_MESSAGES[:force_push_protected_branch]
end end
end end
...@@ -62,15 +62,15 @@ module Gitlab ...@@ -62,15 +62,15 @@ module Gitlab
break if user_access.can_push_to_branch?(branch_name) break if user_access.can_push_to_branch?(branch_name)
unless user_access.can_merge_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 end
unless safe_commit_for_new_protected_branch? 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 end
unless updated_from_web? 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 end
end end
...@@ -78,11 +78,11 @@ module Gitlab ...@@ -78,11 +78,11 @@ module Gitlab
def protected_branch_deletion_checks def protected_branch_deletion_checks
logger.log_timed(LOG_MESSAGES[:protected_branch_deletion_checks]) do logger.log_timed(LOG_MESSAGES[:protected_branch_deletion_checks]) do
unless user_access.can_delete_branch?(branch_name) 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 end
unless updated_from_web? 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 end
end end
...@@ -91,11 +91,11 @@ module Gitlab ...@@ -91,11 +91,11 @@ module Gitlab
logger.log_timed(LOG_MESSAGES[:protected_branch_push_checks]) do logger.log_timed(LOG_MESSAGES[:protected_branch_push_checks]) do
if matching_merge_request? if matching_merge_request?
unless user_access.can_merge_to_branch?(branch_name) || user_access.can_push_to_branch?(branch_name) 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 end
else else
unless user_access.can_push_to_branch?(branch_name) 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 end
end end
......
...@@ -46,7 +46,7 @@ module Gitlab ...@@ -46,7 +46,7 @@ module Gitlab
def validate_diff(diff) def validate_diff(diff)
validations_for_diff.each do |validation| validations_for_diff.each do |validation|
if error = validation.call(diff) if error = validation.call(diff)
raise ::Gitlab::GitAccess::UnauthorizedError, error raise ::Gitlab::GitAccess::ForbiddenError, error
end end
end end
end end
...@@ -77,7 +77,7 @@ module Gitlab ...@@ -77,7 +77,7 @@ module Gitlab
logger.log_timed(LOG_MESSAGES[__method__]) do logger.log_timed(LOG_MESSAGES[__method__]) do
path_validations.each do |validation| path_validations.each do |validation|
if error = validation.call(file_paths) if error = validation.call(file_paths)
raise ::Gitlab::GitAccess::UnauthorizedError, error raise ::Gitlab::GitAccess::ForbiddenError, error
end end
end end
end end
......
...@@ -15,7 +15,7 @@ module Gitlab ...@@ -15,7 +15,7 @@ module Gitlab
lfs_check = Checks::LfsIntegrity.new(project, newrev, logger.time_left) lfs_check = Checks::LfsIntegrity.new(project, newrev, logger.time_left)
if lfs_check.objects_missing? if lfs_check.objects_missing?
raise GitAccess::UnauthorizedError, ERROR_MESSAGE raise GitAccess::ForbiddenError, ERROR_MESSAGE
end end
end end
end end
......
...@@ -6,7 +6,7 @@ module Gitlab ...@@ -6,7 +6,7 @@ module Gitlab
def validate! def validate!
logger.log_timed("Checking if you are allowed to push...") do logger.log_timed("Checking if you are allowed to push...") do
unless can_push? unless can_push?
raise GitAccess::UnauthorizedError, GitAccess::ERROR_MESSAGES[:push_code] raise GitAccess::ForbiddenError, GitAccess::ERROR_MESSAGES[:push_code]
end end
end end
end end
......
...@@ -20,7 +20,7 @@ module Gitlab ...@@ -20,7 +20,7 @@ module Gitlab
logger.log_timed(LOG_MESSAGES[:tag_checks]) do logger.log_timed(LOG_MESSAGES[:tag_checks]) do
if tag_exists? && user_access.cannot_do_action?(:admin_tag) 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
end end
...@@ -33,11 +33,11 @@ module Gitlab ...@@ -33,11 +33,11 @@ module Gitlab
logger.log_timed(LOG_MESSAGES[__method__]) do logger.log_timed(LOG_MESSAGES[__method__]) do
return unless ProtectedTag.protected?(project, tag_name) # rubocop:disable Cop/AvoidReturnFromBlocks return unless ProtectedTag.protected?(project, tag_name) # rubocop:disable Cop/AvoidReturnFromBlocks
raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:update_protected_tag]) if update? raise(GitAccess::ForbiddenError, ERROR_MESSAGES[:update_protected_tag]) if update?
raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_protected_tag]) if deletion? raise(GitAccess::ForbiddenError, ERROR_MESSAGES[:delete_protected_tag]) if deletion?
unless user_access.can_create_tag?(tag_name) 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 end
end end
......
...@@ -322,6 +322,7 @@ module Gitlab ...@@ -322,6 +322,7 @@ module Gitlab
limit: 10, limit: 10,
offset: 0, offset: 0,
path: nil, path: nil,
author: nil,
follow: false, follow: false,
skip_merges: false, skip_merges: false,
after: nil, after: nil,
......
...@@ -6,7 +6,7 @@ module Gitlab ...@@ -6,7 +6,7 @@ module Gitlab
class GitAccess class GitAccess
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
UnauthorizedError = Class.new(StandardError) ForbiddenError = Class.new(StandardError)
NotFoundError = Class.new(StandardError) NotFoundError = Class.new(StandardError)
ProjectCreationError = Class.new(StandardError) ProjectCreationError = Class.new(StandardError)
TimeoutError = Class.new(StandardError) TimeoutError = Class.new(StandardError)
...@@ -125,7 +125,7 @@ module Gitlab ...@@ -125,7 +125,7 @@ module Gitlab
return unless actor.is_a?(Key) return unless actor.is_a?(Key)
unless actor.valid? unless actor.valid?
raise UnauthorizedError, "Your SSH key #{actor.errors[:key].first}." raise ForbiddenError, "Your SSH key #{actor.errors[:key].first}."
end end
end end
...@@ -133,7 +133,7 @@ module Gitlab ...@@ -133,7 +133,7 @@ module Gitlab
return if request_from_ci_build? return if request_from_ci_build?
unless protocol_allowed? 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
end end
...@@ -148,7 +148,7 @@ module Gitlab ...@@ -148,7 +148,7 @@ module Gitlab
unless user_access.allowed? unless user_access.allowed?
message = Gitlab::Auth::UserAccessDeniedReason.new(user).rejection_message message = Gitlab::Auth::UserAccessDeniedReason.new(user).rejection_message
raise UnauthorizedError, message raise ForbiddenError, message
end end
end end
...@@ -156,11 +156,11 @@ module Gitlab ...@@ -156,11 +156,11 @@ module Gitlab
case cmd case cmd
when *DOWNLOAD_COMMANDS when *DOWNLOAD_COMMANDS
unless authentication_abilities.include?(:download_code) || authentication_abilities.include?(:build_download_code) 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 end
when *PUSH_COMMANDS when *PUSH_COMMANDS
unless authentication_abilities.include?(:push_code) unless authentication_abilities.include?(:push_code)
raise UnauthorizedError, ERROR_MESSAGES[:auth_upload] raise ForbiddenError, ERROR_MESSAGES[:auth_upload]
end end
end end
end end
...@@ -189,19 +189,19 @@ module Gitlab ...@@ -189,19 +189,19 @@ module Gitlab
def check_upload_pack_disabled! def check_upload_pack_disabled!
if http? && upload_pack_disabled_over_http? 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
end end
def check_receive_pack_disabled! def check_receive_pack_disabled!
if http? && receive_pack_disabled_over_http? 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
end end
def check_command_existence!(cmd) def check_command_existence!(cmd)
unless ALL_COMMANDS.include?(cmd) unless ALL_COMMANDS.include?(cmd)
raise UnauthorizedError, ERROR_MESSAGES[:command_not_allowed] raise ForbiddenError, ERROR_MESSAGES[:command_not_allowed]
end end
end end
...@@ -209,7 +209,7 @@ module Gitlab ...@@ -209,7 +209,7 @@ module Gitlab
return unless receive_pack?(cmd) return unless receive_pack?(cmd)
if Gitlab::Database.read_only? if Gitlab::Database.read_only?
raise UnauthorizedError, push_to_read_only_message raise ForbiddenError, push_to_read_only_message
end end
end end
...@@ -253,23 +253,23 @@ module Gitlab ...@@ -253,23 +253,23 @@ module Gitlab
guest_can_download_code? guest_can_download_code?
unless passed unless passed
raise UnauthorizedError, ERROR_MESSAGES[:download] raise ForbiddenError, ERROR_MESSAGES[:download]
end end
end end
def check_push_access! def check_push_access!
if project.repository_read_only? if project.repository_read_only?
raise UnauthorizedError, ERROR_MESSAGES[:read_only] raise ForbiddenError, ERROR_MESSAGES[:read_only]
end end
if deploy_key? if deploy_key?
unless deploy_key.can_push_to?(project) unless deploy_key.can_push_to?(project)
raise UnauthorizedError, ERROR_MESSAGES[:deploy_key_upload] raise ForbiddenError, ERROR_MESSAGES[:deploy_key_upload]
end end
elsif user elsif user
# User access is verified in check_change_access! # User access is verified in check_change_access!
else else
raise UnauthorizedError, ERROR_MESSAGES[:upload] raise ForbiddenError, ERROR_MESSAGES[:upload]
end end
check_change_access! check_change_access!
...@@ -284,7 +284,7 @@ module Gitlab ...@@ -284,7 +284,7 @@ module Gitlab
project.any_branch_allows_collaboration?(user_access.user) project.any_branch_allows_collaboration?(user_access.user)
unless can_push unless can_push
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_code] raise ForbiddenError, ERROR_MESSAGES[:push_code]
end end
else else
# If there are worktrees with a HEAD pointing to a non-existent object, # If there are worktrees with a HEAD pointing to a non-existent object,
......
...@@ -19,11 +19,11 @@ module Gitlab ...@@ -19,11 +19,11 @@ module Gitlab
def check_change_access! def check_change_access!
unless user_access.can_do_action?(:create_wiki) unless user_access.can_do_action?(:create_wiki)
raise UnauthorizedError, ERROR_MESSAGES[:write_to_wiki] raise ForbiddenError, ERROR_MESSAGES[:write_to_wiki]
end end
if Gitlab::Database.read_only? if Gitlab::Database.read_only?
raise UnauthorizedError, push_to_read_only_message raise ForbiddenError, push_to_read_only_message
end end
true true
......
...@@ -324,6 +324,7 @@ module Gitlab ...@@ -324,6 +324,7 @@ module Gitlab
request.after = GitalyClient.timestamp(options[:after]) if options[:after] request.after = GitalyClient.timestamp(options[:after]) if options[:after]
request.before = GitalyClient.timestamp(options[:before]) if options[:before] request.before = GitalyClient.timestamp(options[:before]) if options[:before]
request.revision = encode_binary(options[:ref]) if options[:ref] 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.order = options[:order].upcase.sub('DEFAULT', 'NONE') if options[:order].present?
request.paths = encode_repeated(Array(options[:path])) if options[:path].present? request.paths = encode_repeated(Array(options[:path])) if options[:path].present?
......
...@@ -43,7 +43,7 @@ module Gitlab ...@@ -43,7 +43,7 @@ module Gitlab
end end
def config_file 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 end
def version_filename def version_filename
...@@ -77,7 +77,7 @@ module Gitlab ...@@ -77,7 +77,7 @@ module Gitlab
end end
def group_config_file 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 end
end end
......
...@@ -4,8 +4,8 @@ module Gitlab ...@@ -4,8 +4,8 @@ module Gitlab
module ImportExport module ImportExport
class AttributeCleaner class AttributeCleaner
ALLOWED_REFERENCES = [ ALLOWED_REFERENCES = [
*ProjectRelationFactory::PROJECT_REFERENCES, *Gitlab::ImportExport::Project::RelationFactory::PROJECT_REFERENCES,
*ProjectRelationFactory::USER_REFERENCES, *Gitlab::ImportExport::Project::RelationFactory::USER_REFERENCES,
'group_id', 'group_id',
'commit_id', 'commit_id',
'discussion_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 ...@@ -49,7 +49,7 @@ module Gitlab
end end
def project_tree def project_tree
@project_tree ||= Gitlab::ImportExport::ProjectTreeRestorer.new(user: current_user, @project_tree ||= Gitlab::ImportExport::Project::TreeRestorer.new(user: current_user,
shared: shared, shared: shared,
project: project) project: project)
end end
...@@ -125,7 +125,7 @@ module Gitlab ...@@ -125,7 +125,7 @@ module Gitlab
def project_to_overwrite def project_to_overwrite
strong_memoize(:project_to_overwrite) do 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 end
end end
......
...@@ -91,9 +91,9 @@ module Gitlab ...@@ -91,9 +91,9 @@ module Gitlab
def relation_class def relation_class
case @importable case @importable
when Project when ::Project
ProjectMember ProjectMember
when Group when ::Group
GroupMember GroupMember
end end
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 ...@@ -69,7 +69,7 @@ module Gitlab
def process_relation_item!(relation_key, relation_definition, relation_index, data_hash) def process_relation_item!(relation_key, relation_definition, relation_index, data_hash)
relation_object = build_relation(relation_key, relation_definition, data_hash) relation_object = build_relation(relation_key, relation_definition, data_hash)
return unless relation_object 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) relation_object.assign_attributes(importable_class_sym => @importable)
...@@ -110,7 +110,7 @@ module Gitlab ...@@ -110,7 +110,7 @@ module Gitlab
excluded_keys: excluded_keys_for_relation(importable_class_sym)) excluded_keys: excluded_keys_for_relation(importable_class_sym))
@importable.assign_attributes(params) @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 Gitlab::Timeless.timeless(@importable) do
@importable.save! @importable.save!
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
class GitlabDanger class GitlabDanger
LOCAL_RULES ||= %w[ LOCAL_RULES ||= %w[
changes_size changes_size
gemfile
documentation documentation
frozen_string frozen_string
duplicate_yarn_dependencies 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) ...@@ -49,7 +49,8 @@ def jobs_to_run(node_index, node_total)
%w[scripts/lint-conflicts.sh], %w[scripts/lint-conflicts.sh],
%w[scripts/lint-rugged], %w[scripts/lint-rugged],
%w[scripts/frontend/check_no_partial_karma_jest.sh], %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 case node_total
......
...@@ -4,7 +4,7 @@ require 'spec_helper' ...@@ -4,7 +4,7 @@ require 'spec_helper'
describe "Admin Health Check", :feature do describe "Admin Health Check", :feature do
include StubENV include StubENV
set(:admin) { create(:admin) } let_it_be(:admin) { create(:admin) }
before do before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
......
...@@ -3,11 +3,11 @@ ...@@ -3,11 +3,11 @@
require 'spec_helper' require 'spec_helper'
describe 'Multiple Issue Boards', :js do describe 'Multiple Issue Boards', :js do
set(:user) { create(:user) } let_it_be(:user) { create(:user) }
set(:project) { create(:project, :public) } let_it_be(:project) { create(:project, :public) }
set(:planning) { create(:label, project: project, name: 'Planning') } let_it_be(:planning) { create(:label, project: project, name: 'Planning') }
set(:board) { create(:board, name: 'board1', project: project) } let_it_be(:board) { create(:board, name: 'board1', project: project) }
set(:board2) { create(:board, name: 'board2', project: project) } let_it_be(:board2) { create(:board, name: 'board2', project: project) }
let(:parent) { project } let(:parent) { project }
let(:boards_path) { project_boards_path(project) } let(:boards_path) { project_boards_path(project) }
......
...@@ -129,10 +129,10 @@ describe 'Issue Boards new issue', :js do ...@@ -129,10 +129,10 @@ describe 'Issue Boards new issue', :js do
end end
context 'group boards' do context 'group boards' do
set(:group) { create(:group, :public) } let_it_be(:group) { create(:group, :public) }
set(:project) { create(:project, namespace: group) } let_it_be(:project) { create(:project, namespace: group) }
set(:group_board) { create(:board, group: group) } let_it_be(:group_board) { create(:board, group: group) }
set(:list) { create(:list, board: group_board, position: 0) } let_it_be(:list) { create(:list, board: group_board, position: 0) }
context 'for unauthorized users' do context 'for unauthorized users' do
before do before do
......
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
require 'spec_helper' require 'spec_helper'
describe 'Commits' do describe 'Commits' do
let(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
let(:user) { create(:user) } let_it_be(:user) { create(:user) }
describe 'CI' do describe 'CI' do
before do before do
...@@ -183,4 +183,41 @@ describe 'Commits' do ...@@ -183,4 +183,41 @@ describe 'Commits' do
expect(find('.js-project-refs-dropdown')).to have_content branch_name expect(find('.js-project-refs-dropdown')).to have_content branch_name
end end
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 end
...@@ -3,17 +3,17 @@ ...@@ -3,17 +3,17 @@
require 'spec_helper' require 'spec_helper'
describe 'Root explore' do describe 'Root explore' do
set(:public_project) { create(:project, :public) } let_it_be(:public_project) { create(:project, :public) }
set(:archived_project) { create(:project, :archived) } let_it_be(:archived_project) { create(:project, :archived) }
set(:internal_project) { create(:project, :internal) } let_it_be(:internal_project) { create(:project, :internal) }
set(:private_project) { create(:project, :private) } let_it_be(:private_project) { create(:project, :private) }
before do before do
allow(Gitlab).to receive(:com?).and_return(true) allow(Gitlab).to receive(:com?).and_return(true)
end end
context 'when logged in' do context 'when logged in' do
set(:user) { create(:user) } let_it_be(:user) { create(:user) }
before do before do
sign_in(user) sign_in(user)
......
...@@ -3,10 +3,10 @@ ...@@ -3,10 +3,10 @@
require 'spec_helper' require 'spec_helper'
describe 'User explores projects' do describe 'User explores projects' do
set(:archived_project) { create(:project, :archived) } let_it_be(:archived_project) { create(:project, :archived) }
set(:internal_project) { create(:project, :internal) } let_it_be(:internal_project) { create(:project, :internal) }
set(:private_project) { create(:project, :private) } let_it_be(:private_project) { create(:project, :private) }
set(:public_project) { create(:project, :public) } let_it_be(:public_project) { create(:project, :public) }
context 'when not signed in' do context 'when not signed in' do
context 'when viewing public projects' do context 'when viewing public projects' do
...@@ -19,7 +19,7 @@ describe 'User explores projects' do ...@@ -19,7 +19,7 @@ describe 'User explores projects' do
end end
context 'when signed in' do context 'when signed in' do
set(:user) { create(:user) } let_it_be(:user) { create(:user) }
before do before do
sign_in(user) sign_in(user)
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
describe 'Groups > Labels > User sees links to issuables' do describe 'Groups > Labels > User sees links to issuables' do
set(:group) { create(:group, :public) } let_it_be(:group) { create(:group, :public) }
before do before do
create(:group_label, group: group, title: 'bug') create(:group_label, group: group, title: 'bug')
......
...@@ -7,7 +7,7 @@ describe "User views issues" do ...@@ -7,7 +7,7 @@ describe "User views issues" do
let!(:open_issue1) { create(:issue, project: project) } let!(:open_issue1) { create(:issue, project: project) }
let!(:open_issue2) { 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 shared_examples "opens issue from list" do
it "opens issue" do it "opens issue" do
......
...@@ -5,8 +5,7 @@ require 'spec_helper' ...@@ -5,8 +5,7 @@ require 'spec_helper'
describe 'Merge request > User posts notes', :js do describe 'Merge request > User posts notes', :js do
include NoteInteractionHelpers include NoteInteractionHelpers
set(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
let(:user) { project.creator } let(:user) { project.creator }
let(:merge_request) do let(:merge_request) do
create(:merge_request, source_project: project, target_project: project) create(:merge_request, source_project: project, target_project: project)
......
...@@ -10,10 +10,10 @@ describe 'User sorts merge requests' do ...@@ -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') create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test')
end end
set(:user) { create(:user) } let_it_be(:user) { create(:user) }
set(:group) { create(:group) } let_it_be(:group) { create(:group) }
set(:group_member) { create(:group_member, :maintainer, user: user, group: group) } let_it_be(:group_member) { create(:group_member, :maintainer, user: user, group: group) }
set(:project) { create(:project, :public, group: group) } let_it_be(:project) { create(:project, :public, group: group) }
before do before do
sign_in(user) sign_in(user)
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
describe 'User views open merge requests' do describe 'User views open merge requests' do
set(:user) { create(:user) } let_it_be(:user) { create(:user) }
shared_examples_for 'shows merge requests' do shared_examples_for 'shows merge requests' do
it 'shows merge requests' do it 'shows merge requests' do
...@@ -12,7 +12,7 @@ describe 'User views open merge requests' do ...@@ -12,7 +12,7 @@ describe 'User views open merge requests' do
end end
context 'when project is public' do 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 not signed in' do
context "when the target branch is the project's default branch" do context "when the target branch is the project's default branch" do
...@@ -114,7 +114,7 @@ describe 'User views open merge requests' do ...@@ -114,7 +114,7 @@ describe 'User views open merge requests' do
context 'when project is internal' do context 'when project is internal' do
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } 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 context 'when signed in' do
before do before do
......
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
require 'spec_helper' require 'spec_helper'
describe "User creates milestone", :js do describe "User creates milestone", :js do
set(:user) { create(:user) } let_it_be(:user) { create(:user) }
set(:project) { create(:project) } let_it_be(:project) { create(:project) }
before do before do
project.add_developer(user) project.add_developer(user)
......
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
require 'spec_helper' require 'spec_helper'
describe "User edits milestone", :js do describe "User edits milestone", :js do
set(:user) { create(:user) } let_it_be(:user) { create(:user) }
set(:project) { create(:project) } let_it_be(:project) { create(:project) }
set(:milestone) { create(:milestone, project: project, start_date: Date.today, due_date: 5.days.from_now) } let_it_be(:milestone) { create(:milestone, project: project, start_date: Date.today, due_date: 5.days.from_now) }
before do before do
project.add_developer(user) project.add_developer(user)
......
...@@ -3,10 +3,10 @@ ...@@ -3,10 +3,10 @@
require 'spec_helper' require 'spec_helper'
describe 'User promotes milestone' do describe 'User promotes milestone' do
set(:group) { create(:group) } let_it_be(:group) { create(:group) }
set(:user) { create(:user) } let_it_be(:user) { create(:user) }
set(:project) { create(:project, namespace: group) } let_it_be(:project) { create(:project, namespace: group) }
set(:milestone) { create(:milestone, project: project) } let_it_be(:milestone) { create(:milestone, project: project) }
context 'when user can admin group milestones' do context 'when user can admin group milestones' do
before do before do
......
...@@ -3,10 +3,10 @@ ...@@ -3,10 +3,10 @@
require 'spec_helper' require 'spec_helper'
describe "User views milestone" do describe "User views milestone" do
set(:user) { create(:user) } let_it_be(:user) { create(:user) }
set(:project) { create(:project) } let_it_be(:project) { create(:project) }
set(:milestone) { create(:milestone, project: project) } let_it_be(:milestone) { create(:milestone, project: project) }
set(:labels) { create_list(:label, 2, project: project) } let_it_be(:labels) { create_list(:label, 2, project: project) }
before do before do
project.add_developer(user) project.add_developer(user)
......
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
require 'spec_helper' require 'spec_helper'
describe "User views milestones" do describe "User views milestones" do
set(:user) { create(:user) } let_it_be(:user) { create(:user) }
set(:project) { create(:project) } let_it_be(:project) { create(:project) }
set(:milestone) { create(:milestone, project: project) } let_it_be(:milestone) { create(:milestone, project: project) }
before do before do
project.add_developer(user) project.add_developer(user)
...@@ -22,8 +22,8 @@ describe "User views milestones" do ...@@ -22,8 +22,8 @@ describe "User views milestones" do
end end
context "with issues" do context "with issues" do
set(:issue) { create(:issue, project: project, milestone: milestone) } let_it_be(:issue) { create(:issue, project: project, milestone: milestone) }
set(:closed_issue) { create(:closed_issue, project: project, milestone: milestone) } let_it_be(:closed_issue) { create(:closed_issue, project: project, milestone: milestone) }
it "opens milestone" do it "opens milestone" do
click_link(milestone.title) click_link(milestone.title)
...@@ -38,7 +38,7 @@ describe "User views milestones" do ...@@ -38,7 +38,7 @@ describe "User views milestones" do
end end
context "with associated releases" do 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 context "with a single associated release" do
it "shows the associated release" do it "shows the associated release" do
...@@ -48,10 +48,10 @@ describe "User views milestones" do ...@@ -48,10 +48,10 @@ describe "User views milestones" do
end end
context "with lots of associated releases" do 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) } let_it_be(: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) } let_it_be(: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) } let_it_be(: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(: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 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") 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 ...@@ -66,9 +66,9 @@ describe "User views milestones" do
end end
describe "User views milestones with no MR" do describe "User views milestones with no MR" do
set(:user) { create(:user) } let_it_be(:user) { create(:user) }
set(:project) { create(:project, :merge_requests_disabled) } let_it_be(:project) { create(:project, :merge_requests_disabled) }
set(:milestone) { create(:milestone, project: project) } let_it_be(:milestone) { create(:milestone, project: project) }
before do before do
project.add_developer(user) project.add_developer(user)
......
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
require "spec_helper" require "spec_helper"
describe "User downloads artifacts" do describe "User downloads artifacts" do
set(:project) { create(:project, :repository, :public) } let_it_be(:project) { create(:project, :repository, :public) }
set(:pipeline) { create(:ci_empty_pipeline, status: :success, sha: project.commit.id, project: project) } let_it_be(: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(:job) { create(:ci_build, :artifacts, :success, pipeline: pipeline) }
shared_examples "downloading" do shared_examples "downloading" do
it "downloads the zip" do it "downloads the zip" do
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
describe 'Pipeline Badge' do describe 'Pipeline Badge' do
set(:project) { create(:project, :repository, :public) } let_it_be(:project) { create(:project, :repository, :public) }
let(:ref) { project.default_branch } let(:ref) { project.default_branch }
context 'when the project has a pipeline' do context 'when the project has a pipeline' do
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
require "spec_helper" require "spec_helper"
describe "User deletes branch", :js do describe "User deletes branch", :js do
set(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
before do before do
......
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
require "spec_helper" require "spec_helper"
describe "User views branches" do describe "User views branches" do
set(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
set(:user) { project.owner } let_it_be(:user) { project.owner }
before do before do
sign_in(user) sign_in(user)
...@@ -23,7 +23,7 @@ describe "User views branches" do ...@@ -23,7 +23,7 @@ describe "User views branches" do
end end
context "protected branches" do context "protected branches" do
set(:protected_branch) { create(:protected_branch, project: project) } let_it_be(:protected_branch) { create(:protected_branch, project: project) }
before do before do
visit(project_protected_branches_path(project)) visit(project_protected_branches_path(project))
......
...@@ -5,8 +5,8 @@ require 'spec_helper' ...@@ -5,8 +5,8 @@ require 'spec_helper'
describe 'Project > Commit > View user status' do describe 'Project > Commit > View user status' do
include RepoHelpers include RepoHelpers
set(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
set(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:commit_author) { create(:user, email: sample_commit.author_email) } let(:commit_author) { create(:user, email: sample_commit.author_email) }
before do before do
......
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
require "spec_helper" require "spec_helper"
describe "User creates labels" do describe "User creates labels" do
set(:project) { create(:project_empty_repo, :public) } let_it_be(:project) { create(:project_empty_repo, :public) }
set(:user) { create(:user) } let_it_be(:user) { create(:user) }
shared_examples_for "label creation" do shared_examples_for "label creation" do
it "creates new label" do it "creates new label" do
...@@ -66,7 +66,7 @@ describe "User creates labels" do ...@@ -66,7 +66,7 @@ describe "User creates labels" do
end end
context "in another project" do 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 before do
create(:label, project: project, title: "bug") # Create label for `project` (not `another_project`) project. create(:label, project: project, title: "bug") # Create label for `project` (not `another_project`) project.
......
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
require "spec_helper" require "spec_helper"
describe "User edits labels" do describe "User edits labels" do
set(:project) { create(:project_empty_repo, :public) } let_it_be(:project) { create(:project_empty_repo, :public) }
set(:label) { create(:label, project: project) } let_it_be(:label) { create(:label, project: project) }
set(:user) { create(:user) } let_it_be(:user) { create(:user) }
before do before do
project.add_maintainer(user) project.add_maintainer(user)
......
...@@ -3,10 +3,10 @@ ...@@ -3,10 +3,10 @@
require 'spec_helper' require 'spec_helper'
describe 'User promotes label' do describe 'User promotes label' do
set(:group) { create(:group) } let_it_be(:group) { create(:group) }
set(:user) { create(:user) } let_it_be(:user) { create(:user) }
set(:project) { create(:project, namespace: group) } let_it_be(:project) { create(:project, namespace: group) }
set(:label) { create(:label, project: project) } let_it_be(:label) { create(:label, project: project) }
context 'when user can admin group labels' do context 'when user can admin group labels' do
before do before do
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
describe 'Projects > Labels > User sees links to issuables' do describe 'Projects > Labels > User sees links to issuables' do
set(:user) { create(:user) } let_it_be(:user) { create(:user) }
before do before do
label # creates the label label # creates the label
...@@ -50,7 +50,7 @@ describe 'Projects > Labels > User sees links to issuables' do ...@@ -50,7 +50,7 @@ describe 'Projects > Labels > User sees links to issuables' do
end end
context 'with a group label' do 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') } let(:label) { create(:group_label, group: group, title: 'bug') }
context 'when merge requests and issues are enabled for the project' do context 'when merge requests and issues are enabled for the project' do
......
...@@ -3,9 +3,8 @@ ...@@ -3,9 +3,8 @@
require "spec_helper" require "spec_helper"
describe "User views labels" do describe "User views labels" do
set(:project) { create(:project_empty_repo, :public) } let_it_be(:project) { create(:project_empty_repo, :public) }
set(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:label_titles) { %w[bug enhancement feature] } let(:label_titles) { %w[bug enhancement feature] }
let!(:prioritized_label) { create(:label, project: project, title: 'prioritized-label-name', priority: 1) } let!(:prioritized_label) { create(:label, project: project, title: 'prioritized-label-name', priority: 1) }
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
describe 'Projects settings' do describe 'Projects settings' do
set(:project) { create(:project) } let_it_be(:project) { create(:project) }
let(:user) { project.owner } let(:user) { project.owner }
let(:panel) { find('.general-settings', match: :first) } let(:panel) { find('.general-settings', match: :first) }
let(:button) { panel.find('.btn.js-settings-toggle') } let(:button) { panel.find('.btn.js-settings-toggle') }
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
describe 'Projects > Show > User sees Git instructions' do 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 shared_examples_for 'redirects to the sign in page' do
it '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 ...@@ -49,7 +49,7 @@ describe 'Projects > Show > User sees Git instructions' do
context 'when project is public' do context 'when project is public' do
context 'when project has no repo' do context 'when project has no repo' do
set(:project) { create(:project, :public) } let_it_be(:project) { create(:project, :public) }
before do before do
sign_in(project.owner) sign_in(project.owner)
...@@ -60,7 +60,7 @@ describe 'Projects > Show > User sees Git instructions' do ...@@ -60,7 +60,7 @@ describe 'Projects > Show > User sees Git instructions' do
end end
context 'when project is empty' do 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 context 'when not signed in' do
before do before do
...@@ -98,7 +98,7 @@ describe 'Projects > Show > User sees Git instructions' do ...@@ -98,7 +98,7 @@ describe 'Projects > Show > User sees Git instructions' do
end end
context 'when project is not empty' do context 'when project is not empty' do
set(:project) { create(:project, :public, :repository) } let_it_be(:project) { create(:project, :public, :repository) }
before do before do
visit(project_path(project)) visit(project_path(project))
...@@ -141,7 +141,7 @@ describe 'Projects > Show > User sees Git instructions' do ...@@ -141,7 +141,7 @@ describe 'Projects > Show > User sees Git instructions' do
end end
context 'when project is internal' do 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 context 'when not signed in' do
before do before do
...@@ -163,7 +163,7 @@ describe 'Projects > Show > User sees Git instructions' do ...@@ -163,7 +163,7 @@ describe 'Projects > Show > User sees Git instructions' do
end end
context 'when project is private' do context 'when project is private' do
set(:project) { create(:project, :private) } let_it_be(:project) { create(:project, :private) }
before do before do
visit(project_path(project)) visit(project_path(project))
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
describe 'Projects > Show > User sees last commit CI status' do 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 it 'shows the project README', :js do
project.enable_ci project.enable_ci
......
...@@ -3,9 +3,8 @@ ...@@ -3,9 +3,8 @@
require 'spec_helper' require 'spec_helper'
describe 'Projects > Show > User sees README' do describe 'Projects > Show > User sees README' do
set(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, :public) }
set(:project) { create(:project, :repository, :public) }
it 'shows the project README', :js do it 'shows the project README', :js do
visit project_path(project) visit project_path(project)
......
...@@ -3,8 +3,7 @@ ...@@ -3,8 +3,7 @@
require 'spec_helper' require 'spec_helper'
describe 'User sees user popover', :js do describe 'User sees user popover', :js do
set(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
let(:user) { project.creator } let(:user) { project.creator }
let(:merge_request) do let(:merge_request) do
create(:merge_request, source_project: project, target_project: project) create(:merge_request, source_project: project, target_project: project)
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
describe 'Projects > Wiki > User previews markdown changes', :js do 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(: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_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: '[some link](other-page)' }) }
let(:wiki_content) do let(:wiki_content) do
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
require "spec_helper" require "spec_helper"
describe "User creates wiki page" do describe "User creates wiki page" do
include WikiHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
let(:wiki) { ProjectWiki.new(project, user) } let(:wiki) { ProjectWiki.new(project, user) }
let(:project) { create(:project) } let(:project) { create(:project) }
...@@ -14,9 +16,11 @@ describe "User creates wiki page" do ...@@ -14,9 +16,11 @@ describe "User creates wiki page" do
end end
context "when wiki is empty" do context "when wiki is empty" do
before do before do |example|
visit(project_wikis_path(project)) visit(project_wikis_path(project))
wait_for_svg_to_be_loaded(example)
click_link "Create your first page" click_link "Create your first page"
end end
...@@ -45,7 +49,7 @@ describe "User creates wiki page" do ...@@ -45,7 +49,7 @@ describe "User creates wiki page" do
expect(page).to have_content("Create New Page") expect(page).to have_content("Create New Page")
end 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") fill_in(:wiki_title, with: "one/two/three-test")
page.within(".wiki-form") do page.within(".wiki-form") do
...@@ -163,7 +167,7 @@ describe "User creates wiki page" 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") expect(page).to have_link('Link to Home', href: "/#{project.full_path}/-/wikis/home")
end end
it_behaves_like 'wiki file attachments', :quarantine it_behaves_like 'wiki file attachments'
end end
context "in a group namespace", :js do context "in a group namespace", :js do
...@@ -175,7 +179,7 @@ describe "User creates wiki page" do ...@@ -175,7 +179,7 @@ describe "User creates wiki page" do
expect(page).to have_field("wiki[message]", with: "Create home") expect(page).to have_field("wiki[message]", with: "Create home")
end 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 page.within(".wiki-form") do
fill_in(:wiki_content, with: "My awesome wiki!") fill_in(:wiki_content, with: "My awesome wiki!")
......
...@@ -19,9 +19,12 @@ describe 'User views a wiki page' do ...@@ -19,9 +19,12 @@ describe 'User views a wiki page' do
sign_in(user) sign_in(user)
end end
context 'when wiki is empty' do context 'when wiki is empty', :js do
before 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" click_link "Create your first page"
fill_in(:wiki_title, with: 'one/two/three-test') fill_in(:wiki_title, with: 'one/two/three-test')
...@@ -32,7 +35,7 @@ describe 'User views a wiki page' do ...@@ -32,7 +35,7 @@ describe 'User views a wiki page' do
end end
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') expect(current_path).to include('one/two/three-test')
first(:link, text: 'three').click first(:link, text: 'three').click
...@@ -45,7 +48,7 @@ describe 'User views a wiki page' do ...@@ -45,7 +48,7 @@ describe 'User views a wiki page' do
end end
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(current_path).to include('one/two/three-test')
expect(find('.wiki-pages')).to have_content('three') expect(find('.wiki-pages')).to have_content('three')
...@@ -162,9 +165,12 @@ describe 'User views a wiki page' do ...@@ -162,9 +165,12 @@ describe 'User views a wiki page' do
end end
it 'opens a default wiki page', :js do it 'opens a default wiki page', :js do
visit(project_path(project)) visit project_path(project)
find('.shortcuts-wiki').click find('.shortcuts-wiki').click
wait_for_svg_to_be_loaded
click_link "Create your first page" click_link "Create your first page"
expect(page).to have_content('Create New Page') expect(page).to have_content('Create New Page')
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
describe 'read-only message' do describe 'read-only message' do
set(:user) { create(:user) } let_it_be(:user) { create(:user) }
before do before do
sign_in(user) sign_in(user)
......
...@@ -5,7 +5,7 @@ require 'spec_helper' ...@@ -5,7 +5,7 @@ require 'spec_helper'
describe "Internal Project Access" do describe "Internal Project Access" do
include AccessMatchers 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 "Project should be internal" do
describe '#internal?' do describe '#internal?' do
......
...@@ -5,7 +5,7 @@ require 'spec_helper' ...@@ -5,7 +5,7 @@ require 'spec_helper'
describe "Private Project Access" do describe "Private Project Access" do
include AccessMatchers 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 "Project should be private" do
describe '#private?' do describe '#private?' do
......
...@@ -5,7 +5,7 @@ require 'spec_helper' ...@@ -5,7 +5,7 @@ require 'spec_helper'
describe "Public Project Access" do describe "Public Project Access" do
include AccessMatchers 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 "Project should be public" do
describe '#public?' do describe '#public?' do
......
...@@ -10,10 +10,10 @@ describe "User sorts things" do ...@@ -10,10 +10,10 @@ describe "User sorts things" do
include Spec::Support::Helpers::Features::SortingHelpers include Spec::Support::Helpers::Features::SortingHelpers
include DashboardHelper include DashboardHelper
set(:project) { create(:project_empty_repo, :public) } let_it_be(: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. 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.
set(:issue) { create(:issue, project: project, author: current_user) } let_it_be(: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(:merge_request) { create(:merge_request, target_project: project, source_project: project, author: current_user) }
before do before do
project.add_developer(current_user) project.add_developer(current_user)
......
...@@ -1442,7 +1442,7 @@ TodoService ...@@ -1442,7 +1442,7 @@ TodoService
marks a single todo id as done marks a single todo id as done
caches the number of todos of a user caches the number of todos of a user
Gitlab::ImportExport::ProjectTreeSaver Gitlab::ImportExport::Project::TreeSaver
saves the project tree into a json object saves the project tree into a json object
saves project successfully saves project successfully
JSON JSON
......
...@@ -15,7 +15,7 @@ describe Gitlab::Checks::BranchCheck do ...@@ -15,7 +15,7 @@ describe Gitlab::Checks::BranchCheck do
let(:ref) { 'refs/heads/master' } let(:ref) { 'refs/heads/master' }
it 'raises an error' do 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
end end
...@@ -28,7 +28,7 @@ describe Gitlab::Checks::BranchCheck do ...@@ -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 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(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 end
it 'raises an error if the user is not allowed to merge to protected branches' do 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 ...@@ -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_merge_to_branch?).and_return(false)
expect(user_access).to receive(:can_push_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 end
it 'raises an error if the user is not allowed to push to protected branches' do 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(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 end
context 'when project repository is empty' do context 'when project repository is empty' do
...@@ -58,7 +58,7 @@ describe Gitlab::Checks::BranchCheck do ...@@ -58,7 +58,7 @@ describe Gitlab::Checks::BranchCheck do
end end
it 'raises an error' do 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
end end
...@@ -109,7 +109,7 @@ describe Gitlab::Checks::BranchCheck do ...@@ -109,7 +109,7 @@ describe Gitlab::Checks::BranchCheck do
end end
it 'raises an error' do 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
end end
...@@ -135,7 +135,7 @@ describe Gitlab::Checks::BranchCheck do ...@@ -135,7 +135,7 @@ describe Gitlab::Checks::BranchCheck do
end end
it 'raises an error' do 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
end end
...@@ -157,7 +157,7 @@ describe Gitlab::Checks::BranchCheck do ...@@ -157,7 +157,7 @@ describe Gitlab::Checks::BranchCheck do
context 'via SSH' do context 'via SSH' do
it 'raises an error' 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 end
end end
...@@ -171,7 +171,7 @@ describe Gitlab::Checks::BranchCheck do ...@@ -171,7 +171,7 @@ describe Gitlab::Checks::BranchCheck do
context 'if the user is not allowed to delete protected branches' do context 'if the user is not allowed to delete protected branches' do
it 'raises an error' 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
end end
...@@ -190,7 +190,7 @@ describe Gitlab::Checks::BranchCheck do ...@@ -190,7 +190,7 @@ describe Gitlab::Checks::BranchCheck do
context 'over SSH or HTTP' do context 'over SSH or HTTP' do
it 'raises an error' 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 end
end end
......
...@@ -34,7 +34,7 @@ describe Gitlab::Checks::DiffCheck do ...@@ -34,7 +34,7 @@ describe Gitlab::Checks::DiffCheck do
context 'when change is sent by a different user' 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 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
end end
......
...@@ -50,7 +50,7 @@ describe Gitlab::Checks::LfsCheck do ...@@ -50,7 +50,7 @@ describe Gitlab::Checks::LfsCheck do
end end
it 'fails if any LFS blobs are missing' do 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 end
it 'succeeds if LFS objects have already been uploaded' do it 'succeeds if LFS objects have already been uploaded' do
......
...@@ -15,7 +15,7 @@ describe Gitlab::Checks::PushCheck 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(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(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 end
end end
......
...@@ -11,7 +11,7 @@ describe Gitlab::Checks::TagCheck do ...@@ -11,7 +11,7 @@ describe Gitlab::Checks::TagCheck do
it 'raises an error when user does not have access' 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) 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 end
context 'with protected tag' do context 'with protected tag' do
...@@ -27,7 +27,7 @@ describe Gitlab::Checks::TagCheck do ...@@ -27,7 +27,7 @@ describe Gitlab::Checks::TagCheck do
let(:newrev) { '0000000000000000000000000000000000000000' } let(:newrev) { '0000000000000000000000000000000000000000' }
it 'is prevented' do 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
end end
...@@ -36,7 +36,7 @@ describe Gitlab::Checks::TagCheck do ...@@ -36,7 +36,7 @@ describe Gitlab::Checks::TagCheck do
let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' } let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' }
it 'is prevented' do 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 end
end end
...@@ -47,7 +47,7 @@ describe Gitlab::Checks::TagCheck do ...@@ -47,7 +47,7 @@ describe Gitlab::Checks::TagCheck do
let(:ref) { 'refs/tags/v9.1.0' } let(:ref) { 'refs/tags/v9.1.0' }
it 'prevents creation below access level' do 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 end
context 'when user has access' do context 'when user has access' do
......
...@@ -32,8 +32,8 @@ describe Gitlab::GitAccess do ...@@ -32,8 +32,8 @@ describe Gitlab::GitAccess do
it 'blocks ssh git push and pull' do it 'blocks ssh git push and pull' do
aggregate_failures do aggregate_failures do
expect { push_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_unauthorized('Git access over SSH is not allowed') expect { pull_access_check }.to raise_forbidden('Git access over SSH is not allowed')
end end
end end
end end
...@@ -48,8 +48,8 @@ describe Gitlab::GitAccess do ...@@ -48,8 +48,8 @@ describe Gitlab::GitAccess do
it 'blocks http push and pull' do it 'blocks http push and pull' do
aggregate_failures do aggregate_failures do
expect { push_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_unauthorized('Git access over HTTP is not allowed') expect { pull_access_check }.to raise_forbidden('Git access over HTTP is not allowed')
end end
end end
...@@ -58,7 +58,7 @@ describe Gitlab::GitAccess do ...@@ -58,7 +58,7 @@ describe Gitlab::GitAccess do
it "doesn't block http pull" do it "doesn't block http pull" do
aggregate_failures 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 end
...@@ -67,7 +67,7 @@ describe Gitlab::GitAccess do ...@@ -67,7 +67,7 @@ describe Gitlab::GitAccess do
it "doesn't block http pull" do it "doesn't block http pull" do
aggregate_failures 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 end
end end
...@@ -165,7 +165,7 @@ describe Gitlab::GitAccess do ...@@ -165,7 +165,7 @@ describe Gitlab::GitAccess do
end end
it 'does not block pushes with "not found"' do 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
end end
...@@ -178,7 +178,7 @@ describe Gitlab::GitAccess do ...@@ -178,7 +178,7 @@ describe Gitlab::GitAccess do
end end
it 'blocks the push' do 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
end end
...@@ -208,7 +208,7 @@ describe Gitlab::GitAccess do ...@@ -208,7 +208,7 @@ describe Gitlab::GitAccess do
end end
it 'does not block pushes with "not found"' do 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
end end
...@@ -285,8 +285,8 @@ describe Gitlab::GitAccess do ...@@ -285,8 +285,8 @@ describe Gitlab::GitAccess do
it 'does not allow keys which are too small', :aggregate_failures do it 'does not allow keys which are too small', :aggregate_failures do
expect(actor).not_to be_valid expect(actor).not_to be_valid
expect { pull_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_unauthorized('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
end end
...@@ -297,8 +297,8 @@ describe Gitlab::GitAccess do ...@@ -297,8 +297,8 @@ describe Gitlab::GitAccess do
it 'does not allow keys which are too small', :aggregate_failures do it 'does not allow keys which are too small', :aggregate_failures do
expect(actor).not_to be_valid expect(actor).not_to be_valid
expect { pull_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_unauthorized(/Your SSH key type is forbidden/) expect { push_access_check }.to raise_forbidden(/Your SSH key type is forbidden/)
end end
end end
end end
...@@ -363,7 +363,7 @@ describe Gitlab::GitAccess do ...@@ -363,7 +363,7 @@ describe Gitlab::GitAccess do
let(:authentication_abilities) { [] } let(:authentication_abilities) { [] }
it 'raises unauthorized with download error' do 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 end
context 'when authentication abilities include download code' do context 'when authentication abilities include download code' do
...@@ -387,7 +387,7 @@ describe Gitlab::GitAccess do ...@@ -387,7 +387,7 @@ describe Gitlab::GitAccess do
let(:authentication_abilities) { [] } let(:authentication_abilities) { [] }
it 'raises unauthorized with push error' do 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 end
context 'when authentication abilities include push code' do context 'when authentication abilities include push code' do
...@@ -414,7 +414,7 @@ describe Gitlab::GitAccess do ...@@ -414,7 +414,7 @@ describe Gitlab::GitAccess do
end end
context 'when calling git-upload-pack' do 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 end
context 'when calling git-receive-pack' do context 'when calling git-receive-pack' do
...@@ -428,7 +428,7 @@ describe Gitlab::GitAccess do ...@@ -428,7 +428,7 @@ describe Gitlab::GitAccess do
end end
context 'when calling git-receive-pack' do 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 end
context 'when calling git-upload-pack' do context 'when calling git-upload-pack' do
...@@ -445,7 +445,7 @@ describe Gitlab::GitAccess do ...@@ -445,7 +445,7 @@ describe Gitlab::GitAccess do
allow(Gitlab::Database).to receive(:read_only?) { true } allow(Gitlab::Database).to receive(:read_only?) { true }
end 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
end end
...@@ -559,21 +559,21 @@ describe Gitlab::GitAccess do ...@@ -559,21 +559,21 @@ describe Gitlab::GitAccess do
it 'disallows guests to pull' do it 'disallows guests to pull' do
project.add_guest(user) 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 end
it 'disallows blocked users to pull' do it 'disallows blocked users to pull' do
project.add_maintainer(user) project.add_maintainer(user)
user.block 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 end
it 'disallows deactivated users to pull' do it 'disallows deactivated users to pull' do
project.add_maintainer(user) project.add_maintainer(user)
user.deactivate! 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 end
context 'when the project repository does not exist' do context 'when the project repository does not exist' do
...@@ -610,7 +610,7 @@ describe Gitlab::GitAccess do ...@@ -610,7 +610,7 @@ describe Gitlab::GitAccess do
it 'does not give access to download code' do it 'does not give access to download code' do
public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED) 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 end
end end
...@@ -722,7 +722,7 @@ describe Gitlab::GitAccess do ...@@ -722,7 +722,7 @@ describe Gitlab::GitAccess do
context 'when is not member of the project' do context 'when is not member of the project' do
context 'pull code' 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 end
end end
...@@ -828,7 +828,7 @@ describe Gitlab::GitAccess do ...@@ -828,7 +828,7 @@ describe Gitlab::GitAccess do
expect(&check).not_to raise_error, expect(&check).not_to raise_error,
-> { "expected #{action} to be allowed" } -> { "expected #{action} to be allowed" }
else else
expect(&check).to raise_error(Gitlab::GitAccess::UnauthorizedError), expect(&check).to raise_error(Gitlab::GitAccess::ForbiddenError),
-> { "expected #{action} to be disallowed" } -> { "expected #{action} to be disallowed" }
end end
end end
...@@ -965,7 +965,7 @@ describe Gitlab::GitAccess do ...@@ -965,7 +965,7 @@ describe Gitlab::GitAccess do
it 'does not allow deactivated users to push' do it 'does not allow deactivated users to push' do
user.deactivate! 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 end
it 'cleans up the files' do it 'cleans up the files' do
...@@ -1009,26 +1009,26 @@ describe Gitlab::GitAccess do ...@@ -1009,26 +1009,26 @@ describe Gitlab::GitAccess do
project.add_reporter(user) project.add_reporter(user)
end 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 end
context 'when unauthorized' do context 'when unauthorized' do
context 'to public project' do context 'to public project' do
let(:project) { create(:project, :public, :repository) } 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 end
context 'to internal project' do context 'to internal project' do
let(:project) { create(:project, :internal, :repository) } 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 end
context 'to private project' do context 'to private project' do
let(:project) { create(:project, :private, :repository) } 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 end
end end
...@@ -1039,7 +1039,7 @@ describe Gitlab::GitAccess do ...@@ -1039,7 +1039,7 @@ describe Gitlab::GitAccess do
it 'denies push access' do it 'denies push access' do
project.add_maintainer(user) 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
end end
...@@ -1060,7 +1060,7 @@ describe Gitlab::GitAccess do ...@@ -1060,7 +1060,7 @@ describe Gitlab::GitAccess do
context 'to public project' do context 'to public project' do
let(:project) { create(:project, :public, :repository) } 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 end
context 'to internal project' do context 'to internal project' do
...@@ -1083,14 +1083,14 @@ describe Gitlab::GitAccess do ...@@ -1083,14 +1083,14 @@ describe Gitlab::GitAccess do
key.deploy_keys_projects.create(project: project, can_push: false) key.deploy_keys_projects.create(project: project, can_push: false)
end 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 end
context 'when unauthorized' do context 'when unauthorized' do
context 'to public project' do context 'to public project' do
let(:project) { create(:project, :public, :repository) } 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 end
context 'to internal project' do context 'to internal project' do
...@@ -1121,7 +1121,7 @@ describe Gitlab::GitAccess do ...@@ -1121,7 +1121,7 @@ describe Gitlab::GitAccess do
it 'blocks access when the user did not accept terms', :aggregate_failures do it 'blocks access when the user did not accept terms', :aggregate_failures do
actions.each do |action| 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
end end
...@@ -1211,8 +1211,8 @@ describe Gitlab::GitAccess do ...@@ -1211,8 +1211,8 @@ describe Gitlab::GitAccess do
access.check('git-receive-pack', changes) access.check('git-receive-pack', changes)
end end
def raise_unauthorized(message) def raise_forbidden(message)
raise_error(Gitlab::GitAccess::UnauthorizedError, message) raise_error(Gitlab::GitAccess::ForbiddenError, message)
end end
def raise_not_found def raise_not_found
......
...@@ -33,7 +33,7 @@ describe Gitlab::GitAccessWiki do ...@@ -33,7 +33,7 @@ describe Gitlab::GitAccessWiki do
end end
it 'does not give access to upload wiki code' do 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 end
end end
...@@ -70,7 +70,7 @@ describe Gitlab::GitAccessWiki do ...@@ -70,7 +70,7 @@ describe Gitlab::GitAccessWiki do
it 'does not give access to download wiki code' do it 'does not give access to download wiki code' do
project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED) 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 end
end end
......
...@@ -306,5 +306,19 @@ describe Gitlab::GitalyClient::CommitService do ...@@ -306,5 +306,19 @@ describe Gitlab::GitalyClient::CommitService do
client.find_commits(order: 'topo') client.find_commits(order: 'topo')
end 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
end end
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe Gitlab::ImportExport::BaseObjectBuilder do describe Gitlab::ImportExport::Base::ObjectBuilder do
let(:project) do let(:project) do
create(:project, :repository, create(:project, :repository,
:builds_disabled, :builds_disabled,
...@@ -11,7 +11,7 @@ describe Gitlab::ImportExport::BaseObjectBuilder do ...@@ -11,7 +11,7 @@ describe Gitlab::ImportExport::BaseObjectBuilder do
path: 'project') path: 'project')
end end
let(:klass) { Milestone } 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) } subject { described_class.build(klass, attributes) }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe Gitlab::ImportExport::BaseRelationFactory do describe Gitlab::ImportExport::Base::RelationFactory do
let(:user) { create(:admin) } let(:user) { create(:admin) }
let(:project) { create(:project) } let(:project) { create(:project) }
let(:members_mapper) { double('members_mapper').as_null_object } let(:members_mapper) { double('members_mapper').as_null_object }
...@@ -13,7 +13,7 @@ describe Gitlab::ImportExport::BaseRelationFactory do ...@@ -13,7 +13,7 @@ describe Gitlab::ImportExport::BaseRelationFactory do
subject do subject do
described_class.create(relation_sym: relation_sym, described_class.create(relation_sym: relation_sym,
relation_hash: relation_hash, relation_hash: relation_hash,
object_builder: Gitlab::ImportExport::GroupProjectObjectBuilder, object_builder: Gitlab::ImportExport::Project::ObjectBuilder,
members_mapper: members_mapper, members_mapper: members_mapper,
user: user, user: user,
importable: project, importable: project,
......
...@@ -24,11 +24,11 @@ describe 'forked project import' do ...@@ -24,11 +24,11 @@ describe 'forked project import' do
end end
let(:saver) do 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 end
let(:restorer) do 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 end
before do before do
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe Gitlab::ImportExport::GroupObjectBuilder do describe Gitlab::ImportExport::Group::ObjectBuilder do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:base_attributes) do let(:base_attributes) do
{ {
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe Gitlab::ImportExport::GroupRelationFactory do describe Gitlab::ImportExport::Group::RelationFactory do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:members_mapper) { double('members_mapper').as_null_object } let(:members_mapper) { double('members_mapper').as_null_object }
let(:user) { create(:admin) } let(:user) { create(:admin) }
...@@ -11,7 +11,7 @@ describe Gitlab::ImportExport::GroupRelationFactory do ...@@ -11,7 +11,7 @@ describe Gitlab::ImportExport::GroupRelationFactory do
described_class.create(relation_sym: relation_sym, described_class.create(relation_sym: relation_sym,
relation_hash: relation_hash, relation_hash: relation_hash,
members_mapper: members_mapper, members_mapper: members_mapper,
object_builder: Gitlab::ImportExport::GroupObjectBuilder, object_builder: Gitlab::ImportExport::Group::ObjectBuilder,
user: user, user: user,
importable: group, importable: group,
excluded_keys: excluded_keys) excluded_keys: excluded_keys)
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe Gitlab::ImportExport::GroupTreeRestorer do describe Gitlab::ImportExport::Group::TreeRestorer do
include ImportExport::CommonUtil include ImportExport::CommonUtil
let(:shared) { Gitlab::ImportExport::Shared.new(group) } let(:shared) { Gitlab::ImportExport::Shared.new(group) }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe Gitlab::ImportExport::GroupTreeSaver do describe Gitlab::ImportExport::Group::TreeSaver do
describe 'saves the group tree into a json object' do describe 'saves the group tree into a json object' do
let(:shared) { Gitlab::ImportExport::Shared.new(group) } let(:shared) { Gitlab::ImportExport::Shared.new(group) }
let(:group_tree_saver) { described_class.new(group: group, current_user: user, shared: shared) } let(:group_tree_saver) { described_class.new(group: group, current_user: user, shared: shared) }
...@@ -72,7 +72,7 @@ describe Gitlab::ImportExport::GroupTreeSaver do ...@@ -72,7 +72,7 @@ describe Gitlab::ImportExport::GroupTreeSaver do
# except: # except:
# context 'with description override' do # context 'with description override' do
# context 'group members' do # context 'group members' do
# ^ These are specific for the groupTreeSaver # ^ These are specific for the Group::TreeSaver
context 'JSON' do context 'JSON' do
let(:saved_group_json) do let(:saved_group_json) do
group_tree_saver.save group_tree_saver.save
......
...@@ -63,7 +63,7 @@ describe Gitlab::ImportExport::Importer do ...@@ -63,7 +63,7 @@ describe Gitlab::ImportExport::Importer do
end end
it 'restores the ProjectTree' do 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 importer.execute
end end
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe Gitlab::ImportExport::GroupProjectObjectBuilder do describe Gitlab::ImportExport::Project::ObjectBuilder do
let!(:group) { create(:group, :private) } let!(:group) { create(:group, :private) }
let!(:subgroup) { create(:group, :private, parent: group) } let!(:subgroup) { create(:group, :private, parent: group) }
let!(:project) do let!(:project) do
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe Gitlab::ImportExport::ProjectRelationFactory do describe Gitlab::ImportExport::Project::RelationFactory do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:project) { create(:project, :repository, group: group) } let(:project) { create(:project, :repository, group: group) }
let(:members_mapper) { double('members_mapper').as_null_object } let(:members_mapper) { double('members_mapper').as_null_object }
...@@ -11,7 +11,7 @@ describe Gitlab::ImportExport::ProjectRelationFactory do ...@@ -11,7 +11,7 @@ describe Gitlab::ImportExport::ProjectRelationFactory do
let(:created_object) do let(:created_object) do
described_class.create(relation_sym: relation_sym, described_class.create(relation_sym: relation_sym,
relation_hash: relation_hash, relation_hash: relation_hash,
object_builder: Gitlab::ImportExport::GroupProjectObjectBuilder, object_builder: Gitlab::ImportExport::Project::ObjectBuilder,
members_mapper: members_mapper, members_mapper: members_mapper,
user: user, user: user,
importable: project, importable: project,
...@@ -243,11 +243,11 @@ describe Gitlab::ImportExport::ProjectRelationFactory do ...@@ -243,11 +243,11 @@ describe Gitlab::ImportExport::ProjectRelationFactory do
context 'Project references' do context 'Project references' do
let(:relation_sym) { :project_foo_model } let(:relation_sym) { :project_foo_model }
let(:relation_hash) do let(:relation_hash) do
Gitlab::ImportExport::ProjectRelationFactory::PROJECT_REFERENCES.map { |ref| { ref => 99 } }.inject(:merge) Gitlab::ImportExport::Project::RelationFactory::PROJECT_REFERENCES.map { |ref| { ref => 99 } }.inject(:merge)
end end
class ProjectFooModel < FooModel class ProjectFooModel < FooModel
attr_accessor(*Gitlab::ImportExport::ProjectRelationFactory::PROJECT_REFERENCES) attr_accessor(*Gitlab::ImportExport::Project::RelationFactory::PROJECT_REFERENCES)
end end
before do before do
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' 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(:fixture) { 'spec/fixtures/lib/gitlab/import_export/with_duplicates.json' }
let(:project_tree) { JSON.parse(File.read(fixture)) } let(:project_tree) { JSON.parse(File.read(fixture)) }
......
# frozen_string_literal: true # frozen_string_literal: true
require 'spec_helper' require 'spec_helper'
include ImportExport::CommonUtil
describe Gitlab::ImportExport::ProjectTreeRestorer do describe Gitlab::ImportExport::Project::TreeRestorer do
include ImportExport::CommonUtil include ImportExport::CommonUtil
let(:shared) { project.import_export_shared } let(:shared) { project.import_export_shared }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe Gitlab::ImportExport::ProjectTreeSaver do describe Gitlab::ImportExport::Project::TreeSaver do
describe 'saves the project tree into a json object' do describe 'saves the project tree into a json object' do
let(:shared) { project.import_export_shared } let(:shared) { project.import_export_shared }
let(:project_tree_saver) { described_class.new(project: project, current_user: user, shared: shared) } let(:project_tree_saver) { described_class.new(project: project, current_user: user, shared: shared) }
...@@ -75,7 +75,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver do ...@@ -75,7 +75,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
# except: # except:
# context 'with description override' do # context 'with description override' do
# context 'group members' do # context 'group members' do
# ^ These are specific for the ProjectTreeSaver # ^ These are specific for the Project::TreeSaver
context 'JSON' do context 'JSON' do
let(:saved_project_json) do let(:saved_project_json) do
project_tree_saver.save project_tree_saver.save
......
...@@ -22,7 +22,7 @@ describe Gitlab::ImportExport::RelationRenameService do ...@@ -22,7 +22,7 @@ describe Gitlab::ImportExport::RelationRenameService do
end end
context 'when importing' do 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(:file_content) { IO.read(File.join(shared.export_path, 'project.json')) }
let(:json_file) { ActiveSupport::JSON.decode(file_content) } let(:json_file) { ActiveSupport::JSON.decode(file_content) }
...@@ -99,7 +99,7 @@ describe Gitlab::ImportExport::RelationRenameService do ...@@ -99,7 +99,7 @@ describe Gitlab::ImportExport::RelationRenameService do
let(:relation_tree_saver) { Gitlab::ImportExport::RelationTreeSaver.new } let(:relation_tree_saver) { Gitlab::ImportExport::RelationTreeSaver.new }
let(:project_tree_saver) do let(:project_tree_saver) do
Gitlab::ImportExport::ProjectTreeSaver.new( Gitlab::ImportExport::Project::TreeSaver.new(
project: project, current_user: user, shared: shared) project: project, current_user: user, shared: shared)
end end
......
...@@ -39,8 +39,8 @@ describe Gitlab::ImportExport::RelationTreeRestorer do ...@@ -39,8 +39,8 @@ describe Gitlab::ImportExport::RelationTreeRestorer do
context 'when restoring a project' do context 'when restoring a project' do
let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/project.json' } let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/project.json' }
let(:importable) { create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') } let(:importable) { create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') }
let(:object_builder) { Gitlab::ImportExport::GroupProjectObjectBuilder } let(:object_builder) { Gitlab::ImportExport::Project::ObjectBuilder }
let(:relation_factory) { Gitlab::ImportExport::ProjectRelationFactory } let(:relation_factory) { Gitlab::ImportExport::Project::RelationFactory }
let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) } let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) }
let(:tree_hash) { importable_hash } let(:tree_hash) { importable_hash }
......
...@@ -9,7 +9,7 @@ describe GitlabDanger do ...@@ -9,7 +9,7 @@ describe GitlabDanger do
describe '.local_warning_message' do describe '.local_warning_message' do
it 'returns an informational message with rules that can run' 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
end end
......
...@@ -320,6 +320,21 @@ describe Repository do ...@@ -320,6 +320,21 @@ describe Repository do
end end
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 context "when 'all' flag is set" do
it 'returns every commit from the repository' do it 'returns every commit from the repository' do
expect(repository.commits(nil, all: true, limit: 60).size).to eq(60) expect(repository.commits(nil, all: true, limit: 60).size).to eq(60)
......
...@@ -409,7 +409,7 @@ describe API::Internal::Base do ...@@ -409,7 +409,7 @@ describe API::Internal::Base do
it do it do
pull(key, project) 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(json_response["status"]).to be_falsey
expect(user.reload.last_activity_on).to be_nil expect(user.reload.last_activity_on).to be_nil
end end
...@@ -419,7 +419,7 @@ describe API::Internal::Base do ...@@ -419,7 +419,7 @@ describe API::Internal::Base do
it do it do
push(key, project) 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(json_response["status"]).to be_falsey
expect(user.reload.last_activity_on).to be_nil expect(user.reload.last_activity_on).to be_nil
end end
...@@ -518,7 +518,7 @@ describe API::Internal::Base do ...@@ -518,7 +518,7 @@ describe API::Internal::Base do
it do it do
pull(key, personal_project) 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(json_response["status"]).to be_falsey
expect(user.reload.last_activity_on).to be_nil expect(user.reload.last_activity_on).to be_nil
end end
...@@ -528,7 +528,7 @@ describe API::Internal::Base do ...@@ -528,7 +528,7 @@ describe API::Internal::Base do
it do it do
push(key, personal_project) 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(json_response["status"]).to be_falsey
expect(user.reload.last_activity_on).to be_nil expect(user.reload.last_activity_on).to be_nil
end end
...@@ -572,7 +572,7 @@ describe API::Internal::Base do ...@@ -572,7 +572,7 @@ describe API::Internal::Base do
it do it do
push(key, project) 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(json_response["status"]).to be_falsey
end end
end end
...@@ -654,7 +654,7 @@ describe API::Internal::Base do ...@@ -654,7 +654,7 @@ describe API::Internal::Base do
it 'rejects the SSH push' do it 'rejects the SSH push' do
push(key, project) 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['status']).to be_falsey
expect(json_response['message']).to eq 'Git access over SSH is not allowed' expect(json_response['message']).to eq 'Git access over SSH is not allowed'
end end
...@@ -662,7 +662,7 @@ describe API::Internal::Base do ...@@ -662,7 +662,7 @@ describe API::Internal::Base do
it 'rejects the SSH pull' do it 'rejects the SSH pull' do
pull(key, project) 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['status']).to be_falsey
expect(json_response['message']).to eq 'Git access over SSH is not allowed' expect(json_response['message']).to eq 'Git access over SSH is not allowed'
end end
...@@ -676,7 +676,7 @@ describe API::Internal::Base do ...@@ -676,7 +676,7 @@ describe API::Internal::Base do
it 'rejects the HTTP push' do it 'rejects the HTTP push' do
push(key, project, 'http') 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['status']).to be_falsey
expect(json_response['message']).to eq 'Git access over HTTP is not allowed' expect(json_response['message']).to eq 'Git access over HTTP is not allowed'
end end
...@@ -684,7 +684,7 @@ describe API::Internal::Base do ...@@ -684,7 +684,7 @@ describe API::Internal::Base do
it 'rejects the HTTP pull' do it 'rejects the HTTP pull' do
pull(key, project, 'http') 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['status']).to be_falsey
expect(json_response['message']).to eq 'Git access over HTTP is not allowed' expect(json_response['message']).to eq 'Git access over HTTP is not allowed'
end end
......
...@@ -7,7 +7,7 @@ describe Groups::ImportExport::ExportService do ...@@ -7,7 +7,7 @@ describe Groups::ImportExport::ExportService do
let!(:user) { create(:user) } let!(:user) { create(:user) }
let(:group) { create(:group) } let(:group) { create(:group) }
let(:shared) { Gitlab::ImportExport::Shared.new(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 }) } let(:service) { described_class.new(group: group, user: user, params: { shared: shared }) }
before do before do
...@@ -15,11 +15,11 @@ describe Groups::ImportExport::ExportService do ...@@ -15,11 +15,11 @@ describe Groups::ImportExport::ExportService do
end end
after do after do
FileUtils.rm_rf(export_path) FileUtils.rm_rf(archive_path)
end end
it 'saves the models' do 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 service.execute
end end
...@@ -29,7 +29,7 @@ describe Groups::ImportExport::ExportService do ...@@ -29,7 +29,7 @@ describe Groups::ImportExport::ExportService do
service.execute service.execute
expect(group.import_export_upload.export_file.file).not_to be_nil 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) expect(File.exist?(shared.archive_path)).to eq(false)
end end
end end
...@@ -46,26 +46,43 @@ describe Groups::ImportExport::ExportService do ...@@ -46,26 +46,43 @@ describe Groups::ImportExport::ExportService do
end end
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 before do
allow(service).to receive_message_chain(:tree_exporter, :save).and_return(false) allow(service).to receive_message_chain(:tree_exporter, :save).and_return(false)
end end
it 'removes the remaining exported data' do 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 { service.execute }.to raise_error(Gitlab::ImportExport::Error)
expect(group.import_export_upload).to be_nil 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) expect(File.exist?(shared.archive_path)).to eq(false)
end end
it 'notifies logger' do 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) expect { service.execute }.to raise_error(Gitlab::ImportExport::Error)
end end
end end
end end
end
end end
...@@ -27,7 +27,7 @@ describe Projects::ImportExport::ExportService do ...@@ -27,7 +27,7 @@ describe Projects::ImportExport::ExportService do
end end
it 'saves the models' do 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 service.execute
end end
...@@ -91,10 +91,10 @@ describe Projects::ImportExport::ExportService do ...@@ -91,10 +91,10 @@ describe Projects::ImportExport::ExportService do
end end
it 'removes the remaining exported data' do 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) 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 end
it 'notifies the user' do it 'notifies the user' do
...@@ -121,10 +121,10 @@ describe Projects::ImportExport::ExportService do ...@@ -121,10 +121,10 @@ describe Projects::ImportExport::ExportService do
end end
it 'removes the remaining exported data' do 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) 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 end
it 'notifies the user' do it 'notifies the user' do
...@@ -142,6 +142,21 @@ describe Projects::ImportExport::ExportService do ...@@ -142,6 +142,21 @@ describe Projects::ImportExport::ExportService do
end end
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 context 'when user does not have admin_project permission' do
let!(:another_user) { create(:user) } let!(:another_user) { create(:user) }
......
...@@ -3,6 +3,11 @@ ...@@ -3,6 +3,11 @@
module WikiHelpers module WikiHelpers
extend self 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) def upload_file_to_wiki(project, user, file_name)
opts = { opts = {
file_name: file_name, file_name: file_name,
......
...@@ -34,13 +34,13 @@ module ImportExport ...@@ -34,13 +34,13 @@ module ImportExport
end end
def get_project_restorer(project, import_path) 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 user: project.creator, shared: get_shared_env(path: import_path), project: project
) )
end end
def get_project_saver(project, export_path) 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) project: project, current_user: project.creator, shared: get_shared_env(path: export_path)
) )
end end
......
...@@ -36,8 +36,8 @@ module ConfigurationHelper ...@@ -36,8 +36,8 @@ module ConfigurationHelper
end end
def relation_class_for_name(relation_name) def relation_class_for_name(relation_name)
relation_name = Gitlab::ImportExport::ProjectRelationFactory.overrides[relation_name.to_sym] || relation_name relation_name = Gitlab::ImportExport::Project::RelationFactory.overrides[relation_name.to_sym] || relation_name
Gitlab::ImportExport::ProjectRelationFactory.relation_class(relation_name) Gitlab::ImportExport::Project::RelationFactory.relation_class(relation_name)
end end
def parsed_attributes(relation_name, attributes, config: Gitlab::ImportExport.config_file) def parsed_attributes(relation_name, attributes, config: Gitlab::ImportExport.config_file)
......
...@@ -42,7 +42,7 @@ RSpec.shared_examples 'wiki file attachments' do ...@@ -42,7 +42,7 @@ RSpec.shared_examples 'wiki file attachments' do
end end
end end
context 'uploading is complete', :quarantine do context 'uploading is complete' do
it 'shows "Attach a file" button on uploading complete' do it 'shows "Attach a file" button on uploading complete' do
attach_with_dropzone attach_with_dropzone
wait_for_requests wait_for_requests
......
# frozen_string_literal: true # 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) # of EE-specific features)
RSpec.shared_examples 'restores project successfully' do |**results| RSpec.shared_examples 'restores project successfully' do |**results|
it 'restores the project' do 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