diff options
134 files changed, 1696 insertions, 1549 deletions
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index 15bb35dd0be..b161e44660e 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -64,10 +64,13 @@ class Projects::CommitsController < Projects::ApplicationController render_404 unless @path.empty? || request.format == :atom || @repository.blob_at(@commit.id, @path) || @repository.tree(@commit.id, @path).entries.present? @limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i search = params[:search] + author = params[:author] @commits = if search.present? @repository.find_commits_by_message(search, @ref, @path, @limit, @offset) + elsif author.present? + @repository.commits(@ref, author: author, path: @path, limit: @limit, offset: @offset) else @repository.commits(@ref, path: @path, limit: @limit, offset: @offset) end diff --git a/app/controllers/repositories/git_http_controller.rb b/app/controllers/repositories/git_http_controller.rb index 75c79881264..5c2b6089bff 100644 --- a/app/controllers/repositories/git_http_controller.rb +++ b/app/controllers/repositories/git_http_controller.rb @@ -7,7 +7,7 @@ module Repositories before_action :access_check prepend_before_action :deny_head_requests, only: [:info_refs] - rescue_from Gitlab::GitAccess::UnauthorizedError, with: :render_403_with_exception + rescue_from Gitlab::GitAccess::ForbiddenError, with: :render_403_with_exception rescue_from Gitlab::GitAccess::NotFoundError, with: :render_404_with_exception rescue_from Gitlab::GitAccess::ProjectCreationError, with: :render_422_with_exception rescue_from Gitlab::GitAccess::TimeoutError, with: :render_503_with_exception diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 4ae64b6c8f1..869a2e8da20 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -599,7 +599,7 @@ module Ci # Manually set the notes for a Ci::Pipeline # There is no ActiveRecord relation between Ci::Pipeline and notes # as they are related to a commit sha. This method helps importing - # them using the +Gitlab::ImportExport::ProjectRelationFactory+ class. + # them using the +Gitlab::ImportExport::Project::RelationFactory+ class. def notes=(notes) notes.each do |note| note[:id] = nil diff --git a/app/models/repository.rb b/app/models/repository.rb index cddffa9bb1d..a53850bb068 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -139,6 +139,7 @@ class Repository repo: raw_repository, ref: ref, path: opts[:path], + author: opts[:author], follow: Array(opts[:path]).length == 1, limit: opts[:limit], offset: opts[:offset], diff --git a/app/services/groups/import_export/export_service.rb b/app/services/groups/import_export/export_service.rb index 2c3975961a8..aa484e7203c 100644 --- a/app/services/groups/import_export/export_service.rb +++ b/app/services/groups/import_export/export_service.rb @@ -18,6 +18,8 @@ module Groups end save! + ensure + cleanup end private @@ -28,7 +30,7 @@ module Groups if savers.all?(&:save) notify_success else - cleanup_and_notify_error! + notify_error! end end @@ -37,21 +39,19 @@ module Groups end def tree_exporter - Gitlab::ImportExport::GroupTreeSaver.new(group: @group, current_user: @current_user, shared: @shared, params: @params) + Gitlab::ImportExport::Group::TreeSaver.new(group: @group, current_user: @current_user, shared: @shared, params: @params) end def file_saver Gitlab::ImportExport::Saver.new(exportable: @group, shared: @shared) end - def cleanup_and_notify_error - FileUtils.rm_rf(shared.export_path) - - notify_error + def cleanup + FileUtils.rm_rf(shared.archive_path) if shared&.archive_path end - def cleanup_and_notify_error! - cleanup_and_notify_error + def notify_error! + notify_error raise Gitlab::ImportExport::Error.new(shared.errors.to_sentence) end diff --git a/app/services/groups/import_export/import_service.rb b/app/services/groups/import_export/import_service.rb index 628c8f5bac0..57d2d9855d1 100644 --- a/app/services/groups/import_export/import_service.rb +++ b/app/services/groups/import_export/import_service.rb @@ -34,7 +34,7 @@ module Groups end def restorer - @restorer ||= Gitlab::ImportExport::GroupTreeRestorer.new(user: @current_user, + @restorer ||= Gitlab::ImportExport::Group::TreeRestorer.new(user: @current_user, shared: @shared, group: @group, group_hash: nil) diff --git a/app/services/lfs/lock_file_service.rb b/app/services/lfs/lock_file_service.rb index 383a0d6b4e3..1b283018c16 100644 --- a/app/services/lfs/lock_file_service.rb +++ b/app/services/lfs/lock_file_service.rb @@ -4,13 +4,13 @@ module Lfs class LockFileService < BaseService def execute unless can?(current_user, :push_code, project) - raise Gitlab::GitAccess::UnauthorizedError, 'You have no permissions' + raise Gitlab::GitAccess::ForbiddenError, 'You have no permissions' end create_lock! rescue ActiveRecord::RecordNotUnique error('already locked', 409, current_lock) - rescue Gitlab::GitAccess::UnauthorizedError => ex + rescue Gitlab::GitAccess::ForbiddenError => ex error(ex.message, 403) rescue => ex error(ex.message, 500) diff --git a/app/services/lfs/unlock_file_service.rb b/app/services/lfs/unlock_file_service.rb index ea5a67b727f..a13e89904a0 100644 --- a/app/services/lfs/unlock_file_service.rb +++ b/app/services/lfs/unlock_file_service.rb @@ -4,11 +4,11 @@ module Lfs class UnlockFileService < BaseService def execute unless can?(current_user, :push_code, project) - raise Gitlab::GitAccess::UnauthorizedError, _('You have no permissions') + raise Gitlab::GitAccess::ForbiddenError, _('You have no permissions') end unlock_file - rescue Gitlab::GitAccess::UnauthorizedError => ex + rescue Gitlab::GitAccess::ForbiddenError => ex error(ex.message, 403) rescue ActiveRecord::RecordNotFound error(_('Lock not found'), 404) diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb index 38859c1efa4..77fddc44085 100644 --- a/app/services/projects/import_export/export_service.rb +++ b/app/services/projects/import_export/export_service.rb @@ -14,6 +14,8 @@ module Projects save_all! execute_after_export_action(after_export_strategy) + ensure + cleanup end private @@ -24,7 +26,7 @@ module Projects return unless after_export_strategy unless after_export_strategy.execute(current_user, project) - cleanup_and_notify_error + notify_error end end @@ -33,7 +35,7 @@ module Projects Gitlab::ImportExport::Saver.save(exportable: project, shared: shared) notify_success else - cleanup_and_notify_error! + notify_error! end end @@ -54,7 +56,7 @@ module Projects end def project_tree_saver - Gitlab::ImportExport::ProjectTreeSaver.new(project: project, current_user: current_user, shared: shared, params: params) + Gitlab::ImportExport::Project::TreeSaver.new(project: project, current_user: current_user, shared: shared, params: params) end def uploads_saver @@ -73,16 +75,12 @@ module Projects Gitlab::ImportExport::LfsSaver.new(project: project, shared: shared) end - def cleanup_and_notify_error - Rails.logger.error("Import/Export - Project #{project.name} with ID: #{project.id} export error - #{shared.errors.join(', ')}") # rubocop:disable Gitlab/RailsLogger - - FileUtils.rm_rf(shared.export_path) - - notify_error + def cleanup + FileUtils.rm_rf(shared.archive_path) if shared&.archive_path end - def cleanup_and_notify_error! - cleanup_and_notify_error + def notify_error! + notify_error raise Gitlab::ImportExport::Error.new(shared.errors.to_sentence) end @@ -92,6 +90,8 @@ module Projects end def notify_error + Rails.logger.error("Import/Export - Project #{project.name} with ID: #{project.id} export error - #{shared.errors.join(', ')}") # rubocop:disable Gitlab/RailsLogger + notification_service.project_not_exported(project, current_user, shared.errors) end end diff --git a/changelogs/unreleased/do-not-parse-undefined-severity-confidence.yml b/changelogs/unreleased/do-not-parse-undefined-severity-confidence.yml new file mode 100644 index 00000000000..32efeccf971 --- /dev/null +++ b/changelogs/unreleased/do-not-parse-undefined-severity-confidence.yml @@ -0,0 +1,5 @@ +--- +title: Do not parse undefined severity and confidence from reports +merge_request: 25884 +author: +type: other diff --git a/changelogs/unreleased/find-commits-by-author.yml b/changelogs/unreleased/find-commits-by-author.yml new file mode 100644 index 00000000000..a0ef9c1f3af --- /dev/null +++ b/changelogs/unreleased/find-commits-by-author.yml @@ -0,0 +1,5 @@ +--- +title: Filter commits by author +merge_request: 25597 +author: +type: added diff --git a/changelogs/unreleased/fj-rename-unauthorized-error-to-forbidden-error.yml b/changelogs/unreleased/fj-rename-unauthorized-error-to-forbidden-error.yml new file mode 100644 index 00000000000..ca2c28c13ae --- /dev/null +++ b/changelogs/unreleased/fj-rename-unauthorized-error-to-forbidden-error.yml @@ -0,0 +1,5 @@ +--- +title: Align git returned error codes +merge_request: 25936 +author: +type: changed diff --git a/changelogs/unreleased/georgekoltsov-196188-cleanup-temp-exports.yml b/changelogs/unreleased/georgekoltsov-196188-cleanup-temp-exports.yml new file mode 100644 index 00000000000..d74a628dfe5 --- /dev/null +++ b/changelogs/unreleased/georgekoltsov-196188-cleanup-temp-exports.yml @@ -0,0 +1,5 @@ +--- +title: Ensure temp export data is removed if Group/Project export failed +merge_request: 25828 +author: +type: fixed diff --git a/danger/gemfile/Dangerfile b/danger/gemfile/Dangerfile deleted file mode 100644 index 07c4c07cfe8..00000000000 --- a/danger/gemfile/Dangerfile +++ /dev/null @@ -1,36 +0,0 @@ -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 diff --git a/doc/administration/compliance.md b/doc/administration/compliance.md index 447b69e14b4..52b6867a310 100644 --- a/doc/administration/compliance.md +++ b/doc/administration/compliance.md @@ -17,3 +17,4 @@ GitLab’s [security features](../security/README.md) may also help you meet rel |**[Audit logs](audit_events.md)**<br>To maintain the integrity of your code, GitLab Enterprise Edition Premium gives admins the ability to view any modifications made within the GitLab server in an advanced audit log system, so you can control, analyze, and track every change.|Premium+|| |**[Auditor users](auditor_users.md)**<br>Auditor users are users who are given read-only access to all projects, groups, and other resources on the GitLab instance.|Premium+|| |**[Credentials inventory](../user/admin_area/credentials_inventory.md)**<br>With a credentials inventory, GitLab administrators can keep track of the credentials used by all of the users in their GitLab instance. |Ultimate|| +|**Separation of Duties using [Protected branches](../user/project/protected_branches.md#protected-branches-approval-by-code-owners-premium) and [custom CI Configuration Paths](../user/project/pipelines/settings.md#custom-ci-configuration-path)**<br> GitLab Silver and Premium users can leverage GitLab's cross-project YAML configuration's to define deployers of code and developers of code. View the [Separation of Duties Deploy Project](https://gitlab.com/guided-explorations/separation-of-duties-deploy/blob/master/README.md) and [Separation of Duties Project](https://gitlab.com/guided-explorations/separation-of-duties/blob/master/README.md) to see how to use this set up to define these roles.|Premium+|| diff --git a/doc/administration/packages/index.md b/doc/administration/packages/index.md index 421b70709b5..a12aec3c7b3 100644 --- a/doc/administration/packages/index.md +++ b/doc/administration/packages/index.md @@ -118,7 +118,10 @@ upload packages: #'path_style' => false # If true, use 'host/bucket_name/object' instead of 'bucket_name.host/object'. } ``` - + + NOTE: **Note:** + Some build tools, like Gradle, must make `HEAD` requests to Amazon S3 to pull a dependency’s metadata. The `gitlab_rails['packages_object_store_proxy_download']` property must be set to `true`. Without this setting, GitLab won't act as a proxy to the Amazon S3 service, and will instead return the signed URL. This will cause a `HTTP 403 Forbidden` response, since Amazon S3 expects a signed URL. + 1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect. diff --git a/doc/development/import_project.md b/doc/development/import_project.md index b969cb5f1c4..e92d18b7ace 100644 --- a/doc/development/import_project.md +++ b/doc/development/import_project.md @@ -81,7 +81,7 @@ The last option is to import a project using a Rails console: sudo -u git -H bundle exec rails console RAILS_ENV=production ``` -1. Create a project and run `ProjectTreeRestorer`: +1. Create a project and run `Project::TreeRestorer`: ```ruby shared_class = Struct.new(:export_path) do @@ -98,7 +98,7 @@ The last option is to import a project using a Rails console: begin #Enable Request store RequestStore.begin! - Gitlab::ImportExport::ProjectTreeRestorer.new(user: user, shared: shared, project: project).restore + Gitlab::ImportExport::Project::TreeRestorer.new(user: user, shared: shared, project: project).restore ensure RequestStore.end! RequestStore.clear! @@ -128,11 +128,11 @@ The last option is to import a project using a Rails console: For Performance testing, we should: - Import a quite large project, [`gitlabhq`](https://gitlab.com/gitlab-org/quality/performance-data#gitlab-performance-test-framework-data) should be a good example. -- Measure the execution time of `ProjectTreeRestorer`. +- Measure the execution time of `Project::TreeRestorer`. - Count the number of executed SQL queries during the restore. - Observe the number of GC cycles happening. -You can use this [snippet](https://gitlab.com/gitlab-org/gitlab/snippets/1924954), which will restore the project, and measure the execution time of `ProjectTreeRestorer`, number of SQL queries and number of GC cycles happening. +You can use this [snippet](https://gitlab.com/gitlab-org/gitlab/snippets/1924954), which will restore the project, and measure the execution time of `Project::TreeRestorer`, number of SQL queries and number of GC cycles happening. You can execute the script from the `gdk/gitlab` directory like this: diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md index 7ea7feac9ba..33639c13b9d 100644 --- a/doc/user/clusters/applications.md +++ b/doc/user/clusters/applications.md @@ -257,7 +257,7 @@ use an A record. If your external endpoint is a hostname, use a CNAME record. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21966) in GitLab 12.7. -A Web Application Firewall (WAF) is able to examine traffic being sent/received +A Web Application Firewall (WAF) examines traffic being sent or received, and can block malicious traffic before it reaches your application. The benefits of a WAF are: @@ -266,7 +266,7 @@ of a WAF are: - Access control for your application - Highly configurable logging and blocking rules -Out of the box, GitLab provides you with a WAF known as [`ModSecurity`](https://www.modsecurity.org/) +Out of the box, GitLab provides you with a WAF known as [`ModSecurity`](https://www.modsecurity.org/). ModSecurity is a toolkit for real-time web application monitoring, logging, and access control. With GitLab's offering, the [OWASP's Core Rule Set](https://www.modsecurity.org/CRS/Documentation/), @@ -288,9 +288,6 @@ when installing your [Ingress application](#ingress). If this is your first time using GitLab's WAF, we recommend you follow the [quick start guide](../../topics/web_application_firewall/quick_start_guide.md). -There is a small performance overhead by enabling ModSecurity. However, -if this is considered significant for your application, you can disable it. - There is a small performance overhead by enabling ModSecurity. If this is considered significant for your application, you can disable ModSecurity's rule engine for your deployed application by setting @@ -693,7 +690,7 @@ cilium: ``` The `clusterType` variable enables the recommended Helm variables for -a corresponding cluster type, the default value is blank. You can +a corresponding cluster type. The default value is blank. You can check the recommended variables for each cluster type in the official documentation: @@ -720,13 +717,13 @@ information. By default, Cilium will drop all non-whitelisted packets upon policy deployment. The audit mode is scheduled for release in [Cilium 1.8](https://github.com/cilium/cilium/pull/9970). In the audit -mode non-whitelisted packets will not be dropped, instead audit -notifications will be generated. GitLab provides alternative Docker +mode, non-whitelisted packets will not be dropped, and audit +notifications will be generated instead. GitLab provides alternative Docker images for Cilium with the audit patch included. You can switch to the custom build and enable the audit mode by adding the following to `.gitlab/managed-apps/cilium/values.yaml`: -```yml +```yaml global: registry: registry.gitlab.com/gitlab-org/defend/cilium policyAuditMode: true @@ -737,15 +734,15 @@ agent: ``` The Cilium monitor log for traffic is logged out by the -`cilium-monitor` sidecar container. You can check these logs via: +`cilium-monitor` sidecar container. You can check these logs with the following command: ```shell kubectl -n gitlab-managed-apps logs cilium-XXXX cilium-monitor ``` -You can disable the monitor log via `.gitlab/managed-apps/cilium/values.yaml`: +You can disable the monitor log in `.gitlab/managed-apps/cilium/values.yaml`: -```yml +```yaml agent: monitor: enabled: false diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md index cdf6a789ec2..716078ed1d1 100644 --- a/doc/user/project/settings/import_export.md +++ b/doc/user/project/settings/import_export.md @@ -89,7 +89,7 @@ The following items will NOT be exported: NOTE: **Note:** For more details on the specific data persisted in a project export, see the -[`import_export.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/import_export/import_export.yml) file. +[`import_export.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/import_export/project/import_export.yml) file. ## Exporting a project and its data diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb index 382bbeb66de..577a6e890d7 100644 --- a/lib/api/internal/base.rb +++ b/lib/api/internal/base.rb @@ -49,8 +49,8 @@ module API result = access_checker.check(params[:action], params[:changes]) @project ||= access_checker.project result - rescue Gitlab::GitAccess::UnauthorizedError => e - return response_with_status(code: 401, success: false, message: e.message) + rescue Gitlab::GitAccess::ForbiddenError => e + return response_with_status(code: 403, success: false, message: e.message) rescue Gitlab::GitAccess::TimeoutError => e return response_with_status(code: 503, success: false, message: e.message) rescue Gitlab::GitAccess::NotFoundError => e diff --git a/lib/gitlab/checks/branch_check.rb b/lib/gitlab/checks/branch_check.rb index 4ddc1c718c7..7be0ef05a49 100644 --- a/lib/gitlab/checks/branch_check.rb +++ b/lib/gitlab/checks/branch_check.rb @@ -28,7 +28,7 @@ module Gitlab logger.log_timed(LOG_MESSAGES[:delete_default_branch_check]) do if deletion? && branch_name == project.default_branch - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_default_branch] + raise GitAccess::ForbiddenError, ERROR_MESSAGES[:delete_default_branch] end end @@ -42,7 +42,7 @@ module Gitlab return unless ProtectedBranch.protected?(project, branch_name) # rubocop:disable Cop/AvoidReturnFromBlocks if forced_push? - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:force_push_protected_branch] + raise GitAccess::ForbiddenError, ERROR_MESSAGES[:force_push_protected_branch] end end @@ -62,15 +62,15 @@ module Gitlab break if user_access.can_push_to_branch?(branch_name) unless user_access.can_merge_to_branch?(branch_name) - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:create_protected_branch] + raise GitAccess::ForbiddenError, ERROR_MESSAGES[:create_protected_branch] end unless safe_commit_for_new_protected_branch? - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:invalid_commit_create_protected_branch] + raise GitAccess::ForbiddenError, ERROR_MESSAGES[:invalid_commit_create_protected_branch] end unless updated_from_web? - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_web_create_protected_branch] + raise GitAccess::ForbiddenError, ERROR_MESSAGES[:non_web_create_protected_branch] end end end @@ -78,11 +78,11 @@ module Gitlab def protected_branch_deletion_checks logger.log_timed(LOG_MESSAGES[:protected_branch_deletion_checks]) do unless user_access.can_delete_branch?(branch_name) - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_master_delete_protected_branch] + raise GitAccess::ForbiddenError, ERROR_MESSAGES[:non_master_delete_protected_branch] end unless updated_from_web? - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_web_delete_protected_branch] + raise GitAccess::ForbiddenError, ERROR_MESSAGES[:non_web_delete_protected_branch] end end end @@ -91,11 +91,11 @@ module Gitlab logger.log_timed(LOG_MESSAGES[:protected_branch_push_checks]) do if matching_merge_request? unless user_access.can_merge_to_branch?(branch_name) || user_access.can_push_to_branch?(branch_name) - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:merge_protected_branch] + raise GitAccess::ForbiddenError, ERROR_MESSAGES[:merge_protected_branch] end else unless user_access.can_push_to_branch?(branch_name) - raise GitAccess::UnauthorizedError, push_to_protected_branch_rejected_message + raise GitAccess::ForbiddenError, push_to_protected_branch_rejected_message end end end diff --git a/lib/gitlab/checks/diff_check.rb b/lib/gitlab/checks/diff_check.rb index 5de71addd5f..0eb2b4c79ef 100644 --- a/lib/gitlab/checks/diff_check.rb +++ b/lib/gitlab/checks/diff_check.rb @@ -46,7 +46,7 @@ module Gitlab def validate_diff(diff) validations_for_diff.each do |validation| if error = validation.call(diff) - raise ::Gitlab::GitAccess::UnauthorizedError, error + raise ::Gitlab::GitAccess::ForbiddenError, error end end end @@ -77,7 +77,7 @@ module Gitlab logger.log_timed(LOG_MESSAGES[__method__]) do path_validations.each do |validation| if error = validation.call(file_paths) - raise ::Gitlab::GitAccess::UnauthorizedError, error + raise ::Gitlab::GitAccess::ForbiddenError, error end end end diff --git a/lib/gitlab/checks/lfs_check.rb b/lib/gitlab/checks/lfs_check.rb index 7b013567a03..f81c215d847 100644 --- a/lib/gitlab/checks/lfs_check.rb +++ b/lib/gitlab/checks/lfs_check.rb @@ -15,7 +15,7 @@ module Gitlab lfs_check = Checks::LfsIntegrity.new(project, newrev, logger.time_left) if lfs_check.objects_missing? - raise GitAccess::UnauthorizedError, ERROR_MESSAGE + raise GitAccess::ForbiddenError, ERROR_MESSAGE end end end diff --git a/lib/gitlab/checks/push_check.rb b/lib/gitlab/checks/push_check.rb index 91f8d0bdbc8..7cc5bc56cbb 100644 --- a/lib/gitlab/checks/push_check.rb +++ b/lib/gitlab/checks/push_check.rb @@ -6,7 +6,7 @@ module Gitlab def validate! logger.log_timed("Checking if you are allowed to push...") do unless can_push? - raise GitAccess::UnauthorizedError, GitAccess::ERROR_MESSAGES[:push_code] + raise GitAccess::ForbiddenError, GitAccess::ERROR_MESSAGES[:push_code] end end end diff --git a/lib/gitlab/checks/tag_check.rb b/lib/gitlab/checks/tag_check.rb index ced0612a7a3..a47e55cb160 100644 --- a/lib/gitlab/checks/tag_check.rb +++ b/lib/gitlab/checks/tag_check.rb @@ -20,7 +20,7 @@ module Gitlab logger.log_timed(LOG_MESSAGES[:tag_checks]) do if tag_exists? && user_access.cannot_do_action?(:admin_tag) - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:change_existing_tags] + raise GitAccess::ForbiddenError, ERROR_MESSAGES[:change_existing_tags] end end @@ -33,11 +33,11 @@ module Gitlab logger.log_timed(LOG_MESSAGES[__method__]) do return unless ProtectedTag.protected?(project, tag_name) # rubocop:disable Cop/AvoidReturnFromBlocks - raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:update_protected_tag]) if update? - raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_protected_tag]) if deletion? + raise(GitAccess::ForbiddenError, ERROR_MESSAGES[:update_protected_tag]) if update? + raise(GitAccess::ForbiddenError, ERROR_MESSAGES[:delete_protected_tag]) if deletion? unless user_access.can_create_tag?(tag_name) - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:create_protected_tag] + raise GitAccess::ForbiddenError, ERROR_MESSAGES[:create_protected_tag] end end end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 6bfe744a5cd..29324381cb5 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -322,6 +322,7 @@ module Gitlab limit: 10, offset: 0, path: nil, + author: nil, follow: false, skip_merges: false, after: nil, diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 906350e57c5..d6c87b858a8 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -6,7 +6,7 @@ module Gitlab class GitAccess include Gitlab::Utils::StrongMemoize - UnauthorizedError = Class.new(StandardError) + ForbiddenError = Class.new(StandardError) NotFoundError = Class.new(StandardError) ProjectCreationError = Class.new(StandardError) TimeoutError = Class.new(StandardError) @@ -125,7 +125,7 @@ module Gitlab return unless actor.is_a?(Key) unless actor.valid? - raise UnauthorizedError, "Your SSH key #{actor.errors[:key].first}." + raise ForbiddenError, "Your SSH key #{actor.errors[:key].first}." end end @@ -133,7 +133,7 @@ module Gitlab return if request_from_ci_build? unless protocol_allowed? - raise UnauthorizedError, "Git access over #{protocol.upcase} is not allowed" + raise ForbiddenError, "Git access over #{protocol.upcase} is not allowed" end end @@ -148,7 +148,7 @@ module Gitlab unless user_access.allowed? message = Gitlab::Auth::UserAccessDeniedReason.new(user).rejection_message - raise UnauthorizedError, message + raise ForbiddenError, message end end @@ -156,11 +156,11 @@ module Gitlab case cmd when *DOWNLOAD_COMMANDS unless authentication_abilities.include?(:download_code) || authentication_abilities.include?(:build_download_code) - raise UnauthorizedError, ERROR_MESSAGES[:auth_download] + raise ForbiddenError, ERROR_MESSAGES[:auth_download] end when *PUSH_COMMANDS unless authentication_abilities.include?(:push_code) - raise UnauthorizedError, ERROR_MESSAGES[:auth_upload] + raise ForbiddenError, ERROR_MESSAGES[:auth_upload] end end end @@ -189,19 +189,19 @@ module Gitlab def check_upload_pack_disabled! if http? && upload_pack_disabled_over_http? - raise UnauthorizedError, ERROR_MESSAGES[:upload_pack_disabled_over_http] + raise ForbiddenError, ERROR_MESSAGES[:upload_pack_disabled_over_http] end end def check_receive_pack_disabled! if http? && receive_pack_disabled_over_http? - raise UnauthorizedError, ERROR_MESSAGES[:receive_pack_disabled_over_http] + raise ForbiddenError, ERROR_MESSAGES[:receive_pack_disabled_over_http] end end def check_command_existence!(cmd) unless ALL_COMMANDS.include?(cmd) - raise UnauthorizedError, ERROR_MESSAGES[:command_not_allowed] + raise ForbiddenError, ERROR_MESSAGES[:command_not_allowed] end end @@ -209,7 +209,7 @@ module Gitlab return unless receive_pack?(cmd) if Gitlab::Database.read_only? - raise UnauthorizedError, push_to_read_only_message + raise ForbiddenError, push_to_read_only_message end end @@ -253,23 +253,23 @@ module Gitlab guest_can_download_code? unless passed - raise UnauthorizedError, ERROR_MESSAGES[:download] + raise ForbiddenError, ERROR_MESSAGES[:download] end end def check_push_access! if project.repository_read_only? - raise UnauthorizedError, ERROR_MESSAGES[:read_only] + raise ForbiddenError, ERROR_MESSAGES[:read_only] end if deploy_key? unless deploy_key.can_push_to?(project) - raise UnauthorizedError, ERROR_MESSAGES[:deploy_key_upload] + raise ForbiddenError, ERROR_MESSAGES[:deploy_key_upload] end elsif user # User access is verified in check_change_access! else - raise UnauthorizedError, ERROR_MESSAGES[:upload] + raise ForbiddenError, ERROR_MESSAGES[:upload] end check_change_access! @@ -284,7 +284,7 @@ module Gitlab project.any_branch_allows_collaboration?(user_access.user) unless can_push - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_code] + raise ForbiddenError, ERROR_MESSAGES[:push_code] end else # If there are worktrees with a HEAD pointing to a non-existent object, diff --git a/lib/gitlab/git_access_wiki.rb b/lib/gitlab/git_access_wiki.rb index 3d0db753f6e..aad46937c32 100644 --- a/lib/gitlab/git_access_wiki.rb +++ b/lib/gitlab/git_access_wiki.rb @@ -19,11 +19,11 @@ module Gitlab def check_change_access! unless user_access.can_do_action?(:create_wiki) - raise UnauthorizedError, ERROR_MESSAGES[:write_to_wiki] + raise ForbiddenError, ERROR_MESSAGES[:write_to_wiki] end if Gitlab::Database.read_only? - raise UnauthorizedError, push_to_read_only_message + raise ForbiddenError, push_to_read_only_message end true diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index ac22f5bf419..1f914dc95d1 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -324,7 +324,8 @@ module Gitlab request.after = GitalyClient.timestamp(options[:after]) if options[:after] request.before = GitalyClient.timestamp(options[:before]) if options[:before] request.revision = encode_binary(options[:ref]) if options[:ref] - request.order = options[:order].upcase.sub('DEFAULT', 'NONE') if options[:order].present? + request.author = encode_binary(options[:author]) if options[:author] + request.order = options[:order].upcase.sub('DEFAULT', 'NONE') if options[:order].present? request.paths = encode_repeated(Array(options[:path])) if options[:path].present? diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb index 8ce6549c0c7..1033e6c4e05 100644 --- a/lib/gitlab/import_export.rb +++ b/lib/gitlab/import_export.rb @@ -43,7 +43,7 @@ module Gitlab end def config_file - Rails.root.join('lib/gitlab/import_export/import_export.yml') + Rails.root.join('lib/gitlab/import_export/project/import_export.yml') end def version_filename @@ -77,7 +77,7 @@ module Gitlab end def group_config_file - Rails.root.join('lib/gitlab/import_export/group_import_export.yml') + Rails.root.join('lib/gitlab/import_export/group/import_export.yml') end end end diff --git a/lib/gitlab/import_export/attribute_cleaner.rb b/lib/gitlab/import_export/attribute_cleaner.rb index d1c20dff799..3bfc059dcd3 100644 --- a/lib/gitlab/import_export/attribute_cleaner.rb +++ b/lib/gitlab/import_export/attribute_cleaner.rb @@ -4,8 +4,8 @@ module Gitlab module ImportExport class AttributeCleaner ALLOWED_REFERENCES = [ - *ProjectRelationFactory::PROJECT_REFERENCES, - *ProjectRelationFactory::USER_REFERENCES, + *Gitlab::ImportExport::Project::RelationFactory::PROJECT_REFERENCES, + *Gitlab::ImportExport::Project::RelationFactory::USER_REFERENCES, 'group_id', 'commit_id', 'discussion_id', diff --git a/lib/gitlab/import_export/base/object_builder.rb b/lib/gitlab/import_export/base/object_builder.rb new file mode 100644 index 00000000000..109d2e233a5 --- /dev/null +++ b/lib/gitlab/import_export/base/object_builder.rb @@ -0,0 +1,105 @@ +# 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 diff --git a/lib/gitlab/import_export/base/relation_factory.rb b/lib/gitlab/import_export/base/relation_factory.rb new file mode 100644 index 00000000000..688627d1f2f --- /dev/null +++ b/lib/gitlab/import_export/base/relation_factory.rb @@ -0,0 +1,308 @@ +# 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 diff --git a/lib/gitlab/import_export/base_object_builder.rb b/lib/gitlab/import_export/base_object_builder.rb deleted file mode 100644 index ec66b7a7a4f..00000000000 --- a/lib/gitlab/import_export/base_object_builder.rb +++ /dev/null @@ -1,103 +0,0 @@ -# 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 diff --git a/lib/gitlab/import_export/base_relation_factory.rb b/lib/gitlab/import_export/base_relation_factory.rb deleted file mode 100644 index fcb516fb3a1..00000000000 --- a/lib/gitlab/import_export/base_relation_factory.rb +++ /dev/null @@ -1,306 +0,0 @@ -# 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 diff --git a/lib/gitlab/import_export/group_import_export.yml b/lib/gitlab/import_export/group/import_export.yml index d4e0ff12373..d4e0ff12373 100644 --- a/lib/gitlab/import_export/group_import_export.yml +++ b/lib/gitlab/import_export/group/import_export.yml diff --git a/lib/gitlab/import_export/group/object_builder.rb b/lib/gitlab/import_export/group/object_builder.rb new file mode 100644 index 00000000000..e171a31348e --- /dev/null +++ b/lib/gitlab/import_export/group/object_builder.rb @@ -0,0 +1,57 @@ +# 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 diff --git a/lib/gitlab/import_export/group/relation_factory.rb b/lib/gitlab/import_export/group/relation_factory.rb new file mode 100644 index 00000000000..91637161377 --- /dev/null +++ b/lib/gitlab/import_export/group/relation_factory.rb @@ -0,0 +1,42 @@ +# 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 diff --git a/lib/gitlab/import_export/group/tree_restorer.rb b/lib/gitlab/import_export/group/tree_restorer.rb new file mode 100644 index 00000000000..e6f49dcac7a --- /dev/null +++ b/lib/gitlab/import_export/group/tree_restorer.rb @@ -0,0 +1,118 @@ +# 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 diff --git a/lib/gitlab/import_export/group/tree_saver.rb b/lib/gitlab/import_export/group/tree_saver.rb new file mode 100644 index 00000000000..48f6925884b --- /dev/null +++ b/lib/gitlab/import_export/group/tree_saver.rb @@ -0,0 +1,57 @@ +# 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 diff --git a/lib/gitlab/import_export/group_object_builder.rb b/lib/gitlab/import_export/group_object_builder.rb deleted file mode 100644 index 9796bfa07d4..00000000000 --- a/lib/gitlab/import_export/group_object_builder.rb +++ /dev/null @@ -1,55 +0,0 @@ -# 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 diff --git a/lib/gitlab/import_export/group_project_object_builder.rb b/lib/gitlab/import_export/group_project_object_builder.rb deleted file mode 100644 index 9e8f9d11393..00000000000 --- a/lib/gitlab/import_export/group_project_object_builder.rb +++ /dev/null @@ -1,117 +0,0 @@ -# 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') diff --git a/lib/gitlab/import_export/group_relation_factory.rb b/lib/gitlab/import_export/group_relation_factory.rb deleted file mode 100644 index e3597af44d2..00000000000 --- a/lib/gitlab/import_export/group_relation_factory.rb +++ /dev/null @@ -1,40 +0,0 @@ -# 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 diff --git a/lib/gitlab/import_export/group_tree_restorer.rb b/lib/gitlab/import_export/group_tree_restorer.rb deleted file mode 100644 index 2f42843ed6c..00000000000 --- a/lib/gitlab/import_export/group_tree_restorer.rb +++ /dev/null @@ -1,116 +0,0 @@ -# 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 diff --git a/lib/gitlab/import_export/group_tree_saver.rb b/lib/gitlab/import_export/group_tree_saver.rb deleted file mode 100644 index 2effcd01e30..00000000000 --- a/lib/gitlab/import_export/group_tree_saver.rb +++ /dev/null @@ -1,55 +0,0 @@ -# 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 diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb index a6463ed678c..4eeecc14067 100644 --- a/lib/gitlab/import_export/importer.rb +++ b/lib/gitlab/import_export/importer.rb @@ -49,7 +49,7 @@ module Gitlab end def project_tree - @project_tree ||= Gitlab::ImportExport::ProjectTreeRestorer.new(user: current_user, + @project_tree ||= Gitlab::ImportExport::Project::TreeRestorer.new(user: current_user, shared: shared, project: project) end @@ -125,7 +125,7 @@ module Gitlab def project_to_overwrite strong_memoize(:project_to_overwrite) do - Project.find_by_full_path("#{project.namespace.full_path}/#{original_path}") + ::Project.find_by_full_path("#{project.namespace.full_path}/#{original_path}") end end end diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb index e7eae0a8c31..fd76252eb36 100644 --- a/lib/gitlab/import_export/members_mapper.rb +++ b/lib/gitlab/import_export/members_mapper.rb @@ -91,9 +91,9 @@ module Gitlab def relation_class case @importable - when Project + when ::Project ProjectMember - when Group + when ::Group GroupMember end end diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/project/import_export.yml index 4fa909ac94b..4fa909ac94b 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/project/import_export.yml diff --git a/lib/gitlab/import_export/project/object_builder.rb b/lib/gitlab/import_export/project/object_builder.rb new file mode 100644 index 00000000000..c3637b1c115 --- /dev/null +++ b/lib/gitlab/import_export/project/object_builder.rb @@ -0,0 +1,119 @@ +# 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') diff --git a/lib/gitlab/import_export/project/relation_factory.rb b/lib/gitlab/import_export/project/relation_factory.rb new file mode 100644 index 00000000000..951482a933a --- /dev/null +++ b/lib/gitlab/import_export/project/relation_factory.rb @@ -0,0 +1,162 @@ +# 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 diff --git a/lib/gitlab/import_export/project/tree_loader.rb b/lib/gitlab/import_export/project/tree_loader.rb new file mode 100644 index 00000000000..6d4737a2d00 --- /dev/null +++ b/lib/gitlab/import_export/project/tree_loader.rb @@ -0,0 +1,74 @@ +# 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 diff --git a/lib/gitlab/import_export/project/tree_restorer.rb b/lib/gitlab/import_export/project/tree_restorer.rb new file mode 100644 index 00000000000..a5123f16dbc --- /dev/null +++ b/lib/gitlab/import_export/project/tree_restorer.rb @@ -0,0 +1,94 @@ +# 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 diff --git a/lib/gitlab/import_export/project/tree_saver.rb b/lib/gitlab/import_export/project/tree_saver.rb new file mode 100644 index 00000000000..58f33a04851 --- /dev/null +++ b/lib/gitlab/import_export/project/tree_saver.rb @@ -0,0 +1,70 @@ +# 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 diff --git a/lib/gitlab/import_export/project_relation_factory.rb b/lib/gitlab/import_export/project_relation_factory.rb deleted file mode 100644 index 0e08a66b89c..00000000000 --- a/lib/gitlab/import_export/project_relation_factory.rb +++ /dev/null @@ -1,160 +0,0 @@ -# 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 diff --git a/lib/gitlab/import_export/project_tree_loader.rb b/lib/gitlab/import_export/project_tree_loader.rb deleted file mode 100644 index fc21858043d..00000000000 --- a/lib/gitlab/import_export/project_tree_loader.rb +++ /dev/null @@ -1,72 +0,0 @@ -# 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 diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb deleted file mode 100644 index aae07657ea0..00000000000 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ /dev/null @@ -1,92 +0,0 @@ -# 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 diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb deleted file mode 100644 index 386a4cfdfc6..00000000000 --- a/lib/gitlab/import_export/project_tree_saver.rb +++ /dev/null @@ -1,68 +0,0 @@ -# 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 diff --git a/lib/gitlab/import_export/relation_tree_restorer.rb b/lib/gitlab/import_export/relation_tree_restorer.rb index 9b84ade1525..1797bbad51a 100644 --- a/lib/gitlab/import_export/relation_tree_restorer.rb +++ b/lib/gitlab/import_export/relation_tree_restorer.rb @@ -69,7 +69,7 @@ module Gitlab def process_relation_item!(relation_key, relation_definition, relation_index, data_hash) relation_object = build_relation(relation_key, relation_definition, data_hash) return unless relation_object - return if importable_class == Project && group_model?(relation_object) + return if importable_class == ::Project && group_model?(relation_object) relation_object.assign_attributes(importable_class_sym => @importable) @@ -110,7 +110,7 @@ module Gitlab excluded_keys: excluded_keys_for_relation(importable_class_sym)) @importable.assign_attributes(params) - @importable.drop_visibility_level! if importable_class == Project + @importable.drop_visibility_level! if importable_class == ::Project Gitlab::Timeless.timeless(@importable) do @importable.save! diff --git a/lib/gitlab_danger.rb b/lib/gitlab_danger.rb index e776e2b7ea3..cf57b20790b 100644 --- a/lib/gitlab_danger.rb +++ b/lib/gitlab_danger.rb @@ -3,7 +3,6 @@ class GitlabDanger LOCAL_RULES ||= %w[ changes_size - gemfile documentation frozen_string duplicate_yarn_dependencies diff --git a/scripts/gemfile_lock_changed.sh b/scripts/gemfile_lock_changed.sh new file mode 100755 index 00000000000..24e2c685f11 --- /dev/null +++ b/scripts/gemfile_lock_changed.sh @@ -0,0 +1,26 @@ +#!/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 diff --git a/scripts/static-analysis b/scripts/static-analysis index 1f55c035ed1..251462fad33 100755 --- a/scripts/static-analysis +++ b/scripts/static-analysis @@ -49,7 +49,8 @@ def jobs_to_run(node_index, node_total) %w[scripts/lint-conflicts.sh], %w[scripts/lint-rugged], %w[scripts/frontend/check_no_partial_karma_jest.sh], - %w[scripts/lint-changelog-filenames] + %w[scripts/lint-changelog-filenames], + %w[scripts/gemfile_lock_changed.sh] ] case node_total diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb index 97c34d55d73..9ce96fe8020 100644 --- a/spec/features/admin/admin_health_check_spec.rb +++ b/spec/features/admin/admin_health_check_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' describe "Admin Health Check", :feature do include StubENV - set(:admin) { create(:admin) } + let_it_be(:admin) { create(:admin) } before do stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') diff --git a/spec/features/boards/multiple_boards_spec.rb b/spec/features/boards/multiple_boards_spec.rb index 2389707be9c..8e56be6bdd0 100644 --- a/spec/features/boards/multiple_boards_spec.rb +++ b/spec/features/boards/multiple_boards_spec.rb @@ -3,11 +3,11 @@ require 'spec_helper' describe 'Multiple Issue Boards', :js do - set(:user) { create(:user) } - set(:project) { create(:project, :public) } - set(:planning) { create(:label, project: project, name: 'Planning') } - set(:board) { create(:board, name: 'board1', project: project) } - set(:board2) { create(:board, name: 'board2', project: project) } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :public) } + let_it_be(:planning) { create(:label, project: project, name: 'Planning') } + let_it_be(:board) { create(:board, name: 'board1', project: project) } + let_it_be(:board2) { create(:board, name: 'board2', project: project) } let(:parent) { project } let(:boards_path) { project_boards_path(project) } diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb index 730887370dd..2d41b5d612d 100644 --- a/spec/features/boards/new_issue_spec.rb +++ b/spec/features/boards/new_issue_spec.rb @@ -129,10 +129,10 @@ describe 'Issue Boards new issue', :js do end context 'group boards' do - set(:group) { create(:group, :public) } - set(:project) { create(:project, namespace: group) } - set(:group_board) { create(:board, group: group) } - set(:list) { create(:list, board: group_board, position: 0) } + let_it_be(:group) { create(:group, :public) } + let_it_be(:project) { create(:project, namespace: group) } + let_it_be(:group_board) { create(:board, group: group) } + let_it_be(:list) { create(:list, board: group_board, position: 0) } context 'for unauthorized users' do before do diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index f538df89fd3..d8b886b239f 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' describe 'Commits' do - let(:project) { create(:project, :repository) } - let(:user) { create(:user) } + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } describe 'CI' do before do @@ -183,4 +183,41 @@ describe 'Commits' do expect(find('.js-project-refs-dropdown')).to have_content branch_name end end + + context 'viewing commits for an author' do + let(:author_commit) { project.repository.commits(nil, limit: 1).first } + let(:commits) { project.repository.commits(nil, author: author, limit: 40) } + + before do + project.add_maintainer(user) + sign_in(user) + visit project_commits_path(project, nil, author: author) + end + + shared_examples 'show commits by author' do + it "includes the author's commits" do + commits.each do |commit| + expect(page).to have_content("#{author_commit.author_name} authored #{commit.authored_date.strftime("%b %d, %Y")}") + end + end + end + + context 'author is complete' do + let(:author) { "#{author_commit.author_name} <#{author_commit.author_email}>" } + + it_behaves_like 'show commits by author' + end + + context 'author is just a name' do + let(:author) { "#{author_commit.author_name}" } + + it_behaves_like 'show commits by author' + end + + context 'author is just an email' do + let(:author) { "#{author_commit.author_email}" } + + it_behaves_like 'show commits by author' + end + end end diff --git a/spec/features/dashboard/root_explore_spec.rb b/spec/features/dashboard/root_explore_spec.rb index 5b686d8b6f1..0e065dbed67 100644 --- a/spec/features/dashboard/root_explore_spec.rb +++ b/spec/features/dashboard/root_explore_spec.rb @@ -3,17 +3,17 @@ require 'spec_helper' describe 'Root explore' do - set(:public_project) { create(:project, :public) } - set(:archived_project) { create(:project, :archived) } - set(:internal_project) { create(:project, :internal) } - set(:private_project) { create(:project, :private) } + let_it_be(:public_project) { create(:project, :public) } + let_it_be(:archived_project) { create(:project, :archived) } + let_it_be(:internal_project) { create(:project, :internal) } + let_it_be(:private_project) { create(:project, :private) } before do allow(Gitlab).to receive(:com?).and_return(true) end context 'when logged in' do - set(:user) { create(:user) } + let_it_be(:user) { create(:user) } before do sign_in(user) diff --git a/spec/features/explore/user_explores_projects_spec.rb b/spec/features/explore/user_explores_projects_spec.rb index 9c3686dba2d..c64709c0b55 100644 --- a/spec/features/explore/user_explores_projects_spec.rb +++ b/spec/features/explore/user_explores_projects_spec.rb @@ -3,10 +3,10 @@ require 'spec_helper' describe 'User explores projects' do - set(:archived_project) { create(:project, :archived) } - set(:internal_project) { create(:project, :internal) } - set(:private_project) { create(:project, :private) } - set(:public_project) { create(:project, :public) } + let_it_be(:archived_project) { create(:project, :archived) } + let_it_be(:internal_project) { create(:project, :internal) } + let_it_be(:private_project) { create(:project, :private) } + let_it_be(:public_project) { create(:project, :public) } context 'when not signed in' do context 'when viewing public projects' do @@ -19,7 +19,7 @@ describe 'User explores projects' do end context 'when signed in' do - set(:user) { create(:user) } + let_it_be(:user) { create(:user) } before do sign_in(user) diff --git a/spec/features/groups/labels/user_sees_links_to_issuables_spec.rb b/spec/features/groups/labels/user_sees_links_to_issuables_spec.rb index 6199b566ebc..38561c71323 100644 --- a/spec/features/groups/labels/user_sees_links_to_issuables_spec.rb +++ b/spec/features/groups/labels/user_sees_links_to_issuables_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'Groups > Labels > User sees links to issuables' do - set(:group) { create(:group, :public) } + let_it_be(:group) { create(:group, :public) } before do create(:group_label, group: group, title: 'bug') diff --git a/spec/features/issues/user_views_issues_spec.rb b/spec/features/issues/user_views_issues_spec.rb index 8f174472f49..796e618c7c8 100644 --- a/spec/features/issues/user_views_issues_spec.rb +++ b/spec/features/issues/user_views_issues_spec.rb @@ -7,7 +7,7 @@ describe "User views issues" do let!(:open_issue1) { create(:issue, project: project) } let!(:open_issue2) { create(:issue, project: project) } - set(:user) { create(:user) } + let_it_be(:user) { create(:user) } shared_examples "opens issue from list" do it "opens issue" do diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb index f24e7090605..b22f5a6c211 100644 --- a/spec/features/merge_request/user_posts_notes_spec.rb +++ b/spec/features/merge_request/user_posts_notes_spec.rb @@ -5,8 +5,7 @@ require 'spec_helper' describe 'Merge request > User posts notes', :js do include NoteInteractionHelpers - set(:project) { create(:project, :repository) } - + let_it_be(:project) { create(:project, :repository) } let(:user) { project.creator } let(:merge_request) do create(:merge_request, source_project: project, target_project: project) diff --git a/spec/features/merge_requests/user_sorts_merge_requests_spec.rb b/spec/features/merge_requests/user_sorts_merge_requests_spec.rb index 3c217786d43..5a84bcb0c44 100644 --- a/spec/features/merge_requests/user_sorts_merge_requests_spec.rb +++ b/spec/features/merge_requests/user_sorts_merge_requests_spec.rb @@ -10,10 +10,10 @@ describe 'User sorts merge requests' do create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test') end - set(:user) { create(:user) } - set(:group) { create(:group) } - set(:group_member) { create(:group_member, :maintainer, user: user, group: group) } - set(:project) { create(:project, :public, group: group) } + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:group_member) { create(:group_member, :maintainer, user: user, group: group) } + let_it_be(:project) { create(:project, :public, group: group) } before do sign_in(user) diff --git a/spec/features/merge_requests/user_views_open_merge_requests_spec.rb b/spec/features/merge_requests/user_views_open_merge_requests_spec.rb index 932090bdbce..4aaa20f0455 100644 --- a/spec/features/merge_requests/user_views_open_merge_requests_spec.rb +++ b/spec/features/merge_requests/user_views_open_merge_requests_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'User views open merge requests' do - set(:user) { create(:user) } + let_it_be(:user) { create(:user) } shared_examples_for 'shows merge requests' do it 'shows merge requests' do @@ -12,7 +12,7 @@ describe 'User views open merge requests' do end context 'when project is public' do - set(:project) { create(:project, :public, :repository) } + let_it_be(:project) { create(:project, :public, :repository) } context 'when not signed in' do context "when the target branch is the project's default branch" do @@ -114,7 +114,7 @@ describe 'User views open merge requests' do context 'when project is internal' do let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } - set(:project) { create(:project, :internal, :repository) } + let_it_be(:project) { create(:project, :internal, :repository) } context 'when signed in' do before do diff --git a/spec/features/milestones/user_creates_milestone_spec.rb b/spec/features/milestones/user_creates_milestone_spec.rb index 5c93ddcf6f8..368a2ddecdf 100644 --- a/spec/features/milestones/user_creates_milestone_spec.rb +++ b/spec/features/milestones/user_creates_milestone_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' describe "User creates milestone", :js do - set(:user) { create(:user) } - set(:project) { create(:project) } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } before do project.add_developer(user) diff --git a/spec/features/milestones/user_edits_milestone_spec.rb b/spec/features/milestones/user_edits_milestone_spec.rb index b41b8f3282f..be05685aff7 100644 --- a/spec/features/milestones/user_edits_milestone_spec.rb +++ b/spec/features/milestones/user_edits_milestone_spec.rb @@ -3,9 +3,9 @@ require 'spec_helper' describe "User edits milestone", :js do - set(:user) { create(:user) } - set(:project) { create(:project) } - set(:milestone) { create(:milestone, project: project, start_date: Date.today, due_date: 5.days.from_now) } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:milestone) { create(:milestone, project: project, start_date: Date.today, due_date: 5.days.from_now) } before do project.add_developer(user) diff --git a/spec/features/milestones/user_promotes_milestone_spec.rb b/spec/features/milestones/user_promotes_milestone_spec.rb index 7678b6cbfa5..d14097e1ef4 100644 --- a/spec/features/milestones/user_promotes_milestone_spec.rb +++ b/spec/features/milestones/user_promotes_milestone_spec.rb @@ -3,10 +3,10 @@ require 'spec_helper' describe 'User promotes milestone' do - set(:group) { create(:group) } - set(:user) { create(:user) } - set(:project) { create(:project, namespace: group) } - set(:milestone) { create(:milestone, project: project) } + let_it_be(:group) { create(:group) } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, namespace: group) } + let_it_be(:milestone) { create(:milestone, project: project) } context 'when user can admin group milestones' do before do diff --git a/spec/features/milestones/user_views_milestone_spec.rb b/spec/features/milestones/user_views_milestone_spec.rb index 71abb195ad1..cbc21dd02e5 100644 --- a/spec/features/milestones/user_views_milestone_spec.rb +++ b/spec/features/milestones/user_views_milestone_spec.rb @@ -3,10 +3,10 @@ require 'spec_helper' describe "User views milestone" do - set(:user) { create(:user) } - set(:project) { create(:project) } - set(:milestone) { create(:milestone, project: project) } - set(:labels) { create_list(:label, 2, project: project) } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:milestone) { create(:milestone, project: project) } + let_it_be(:labels) { create_list(:label, 2, project: project) } before do project.add_developer(user) diff --git a/spec/features/milestones/user_views_milestones_spec.rb b/spec/features/milestones/user_views_milestones_spec.rb index c91fe95aa77..e17797a8165 100644 --- a/spec/features/milestones/user_views_milestones_spec.rb +++ b/spec/features/milestones/user_views_milestones_spec.rb @@ -3,9 +3,9 @@ require 'spec_helper' describe "User views milestones" do - set(:user) { create(:user) } - set(:project) { create(:project) } - set(:milestone) { create(:milestone, project: project) } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:milestone) { create(:milestone, project: project) } before do project.add_developer(user) @@ -22,8 +22,8 @@ describe "User views milestones" do end context "with issues" do - set(:issue) { create(:issue, project: project, milestone: milestone) } - set(:closed_issue) { create(:closed_issue, project: project, milestone: milestone) } + let_it_be(:issue) { create(:issue, project: project, milestone: milestone) } + let_it_be(:closed_issue) { create(:closed_issue, project: project, milestone: milestone) } it "opens milestone" do click_link(milestone.title) @@ -38,7 +38,7 @@ describe "User views milestones" do end context "with associated releases" do - set(:first_release) { create(:release, project: project, name: "The first release", milestones: [milestone], released_at: Time.zone.parse('2019-10-07')) } + let_it_be(:first_release) { create(:release, project: project, name: "The first release", milestones: [milestone], released_at: Time.zone.parse('2019-10-07')) } context "with a single associated release" do it "shows the associated release" do @@ -48,10 +48,10 @@ describe "User views milestones" do end context "with lots of associated releases" do - set(:second_release) { create(:release, project: project, name: "The second release", milestones: [milestone], released_at: first_release.released_at + 1.day) } - set(:third_release) { create(:release, project: project, name: "The third release", milestones: [milestone], released_at: second_release.released_at + 1.day) } - set(:fourth_release) { create(:release, project: project, name: "The fourth release", milestones: [milestone], released_at: third_release.released_at + 1.day) } - set(:fifth_release) { create(:release, project: project, name: "The fifth release", milestones: [milestone], released_at: fourth_release.released_at + 1.day) } + let_it_be(:second_release) { create(:release, project: project, name: "The second release", milestones: [milestone], released_at: first_release.released_at + 1.day) } + let_it_be(:third_release) { create(:release, project: project, name: "The third release", milestones: [milestone], released_at: second_release.released_at + 1.day) } + let_it_be(:fourth_release) { create(:release, project: project, name: "The fourth release", milestones: [milestone], released_at: third_release.released_at + 1.day) } + let_it_be(:fifth_release) { create(:release, project: project, name: "The fifth release", milestones: [milestone], released_at: fourth_release.released_at + 1.day) } it "shows the associated releases and the truncation text" do expect(page).to have_content("Releases #{fifth_release.name} • #{fourth_release.name} • #{third_release.name} • 2 more releases") @@ -66,9 +66,9 @@ describe "User views milestones" do end describe "User views milestones with no MR" do - set(:user) { create(:user) } - set(:project) { create(:project, :merge_requests_disabled) } - set(:milestone) { create(:milestone, project: project) } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :merge_requests_disabled) } + let_it_be(:milestone) { create(:milestone, project: project) } before do project.add_developer(user) diff --git a/spec/features/projects/artifacts/user_downloads_artifacts_spec.rb b/spec/features/projects/artifacts/user_downloads_artifacts_spec.rb index fb70076fcf1..3cbf276c02d 100644 --- a/spec/features/projects/artifacts/user_downloads_artifacts_spec.rb +++ b/spec/features/projects/artifacts/user_downloads_artifacts_spec.rb @@ -3,9 +3,9 @@ require "spec_helper" describe "User downloads artifacts" do - set(:project) { create(:project, :repository, :public) } - set(:pipeline) { create(:ci_empty_pipeline, status: :success, sha: project.commit.id, project: project) } - set(:job) { create(:ci_build, :artifacts, :success, pipeline: pipeline) } + let_it_be(:project) { create(:project, :repository, :public) } + let_it_be(:pipeline) { create(:ci_empty_pipeline, status: :success, sha: project.commit.id, project: project) } + let_it_be(:job) { create(:ci_build, :artifacts, :success, pipeline: pipeline) } shared_examples "downloading" do it "downloads the zip" do diff --git a/spec/features/projects/badges/pipeline_badge_spec.rb b/spec/features/projects/badges/pipeline_badge_spec.rb index 5ddaf1e1591..b2f09a9d0b7 100644 --- a/spec/features/projects/badges/pipeline_badge_spec.rb +++ b/spec/features/projects/badges/pipeline_badge_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'Pipeline Badge' do - set(:project) { create(:project, :repository, :public) } + let_it_be(:project) { create(:project, :repository, :public) } let(:ref) { project.default_branch } context 'when the project has a pipeline' do diff --git a/spec/features/projects/branches/user_deletes_branch_spec.rb b/spec/features/projects/branches/user_deletes_branch_spec.rb index ad63a75a149..184954c1c78 100644 --- a/spec/features/projects/branches/user_deletes_branch_spec.rb +++ b/spec/features/projects/branches/user_deletes_branch_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" describe "User deletes branch", :js do - set(:user) { create(:user) } + let_it_be(:user) { create(:user) } let(:project) { create(:project, :repository) } before do diff --git a/spec/features/projects/branches/user_views_branches_spec.rb b/spec/features/projects/branches/user_views_branches_spec.rb index f3810611094..e127e784b94 100644 --- a/spec/features/projects/branches/user_views_branches_spec.rb +++ b/spec/features/projects/branches/user_views_branches_spec.rb @@ -3,8 +3,8 @@ require "spec_helper" describe "User views branches" do - set(:project) { create(:project, :repository) } - set(:user) { project.owner } + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { project.owner } before do sign_in(user) @@ -23,7 +23,7 @@ describe "User views branches" do end context "protected branches" do - set(:protected_branch) { create(:protected_branch, project: project) } + let_it_be(:protected_branch) { create(:protected_branch, project: project) } before do visit(project_protected_branches_path(project)) diff --git a/spec/features/projects/commit/user_views_user_status_on_commit_spec.rb b/spec/features/projects/commit/user_views_user_status_on_commit_spec.rb index e78b7f7ae08..c07f6081d2c 100644 --- a/spec/features/projects/commit/user_views_user_status_on_commit_spec.rb +++ b/spec/features/projects/commit/user_views_user_status_on_commit_spec.rb @@ -5,8 +5,8 @@ require 'spec_helper' describe 'Project > Commit > View user status' do include RepoHelpers - set(:project) { create(:project, :repository) } - set(:user) { create(:user) } + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } let(:commit_author) { create(:user, email: sample_commit.author_email) } before do diff --git a/spec/features/projects/labels/user_creates_labels_spec.rb b/spec/features/projects/labels/user_creates_labels_spec.rb index 257e064ae3d..180cd8eff14 100644 --- a/spec/features/projects/labels/user_creates_labels_spec.rb +++ b/spec/features/projects/labels/user_creates_labels_spec.rb @@ -3,8 +3,8 @@ require "spec_helper" describe "User creates labels" do - set(:project) { create(:project_empty_repo, :public) } - set(:user) { create(:user) } + let_it_be(:project) { create(:project_empty_repo, :public) } + let_it_be(:user) { create(:user) } shared_examples_for "label creation" do it "creates new label" do @@ -66,7 +66,7 @@ describe "User creates labels" do end context "in another project" do - set(:another_project) { create(:project_empty_repo, :public) } + let_it_be(:another_project) { create(:project_empty_repo, :public) } before do create(:label, project: project, title: "bug") # Create label for `project` (not `another_project`) project. diff --git a/spec/features/projects/labels/user_edits_labels_spec.rb b/spec/features/projects/labels/user_edits_labels_spec.rb index da33ae3af3a..add959ccda6 100644 --- a/spec/features/projects/labels/user_edits_labels_spec.rb +++ b/spec/features/projects/labels/user_edits_labels_spec.rb @@ -3,9 +3,9 @@ require "spec_helper" describe "User edits labels" do - set(:project) { create(:project_empty_repo, :public) } - set(:label) { create(:label, project: project) } - set(:user) { create(:user) } + let_it_be(:project) { create(:project_empty_repo, :public) } + let_it_be(:label) { create(:label, project: project) } + let_it_be(:user) { create(:user) } before do project.add_maintainer(user) diff --git a/spec/features/projects/labels/user_promotes_label_spec.rb b/spec/features/projects/labels/user_promotes_label_spec.rb index fdecafd4c50..cf7320d3cf9 100644 --- a/spec/features/projects/labels/user_promotes_label_spec.rb +++ b/spec/features/projects/labels/user_promotes_label_spec.rb @@ -3,10 +3,10 @@ require 'spec_helper' describe 'User promotes label' do - set(:group) { create(:group) } - set(:user) { create(:user) } - set(:project) { create(:project, namespace: group) } - set(:label) { create(:label, project: project) } + let_it_be(:group) { create(:group) } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, namespace: group) } + let_it_be(:label) { create(:label, project: project) } context 'when user can admin group labels' do before do diff --git a/spec/features/projects/labels/user_sees_links_to_issuables_spec.rb b/spec/features/projects/labels/user_sees_links_to_issuables_spec.rb index 7a9b9e6eac2..f60e7e9703f 100644 --- a/spec/features/projects/labels/user_sees_links_to_issuables_spec.rb +++ b/spec/features/projects/labels/user_sees_links_to_issuables_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'Projects > Labels > User sees links to issuables' do - set(:user) { create(:user) } + let_it_be(:user) { create(:user) } before do label # creates the label @@ -50,7 +50,7 @@ describe 'Projects > Labels > User sees links to issuables' do end context 'with a group label' do - set(:group) { create(:group) } + let_it_be(:group) { create(:group) } let(:label) { create(:group_label, group: group, title: 'bug') } context 'when merge requests and issues are enabled for the project' do diff --git a/spec/features/projects/labels/user_views_labels_spec.rb b/spec/features/projects/labels/user_views_labels_spec.rb index a6f7968c535..7f70ac903d6 100644 --- a/spec/features/projects/labels/user_views_labels_spec.rb +++ b/spec/features/projects/labels/user_views_labels_spec.rb @@ -3,9 +3,8 @@ require "spec_helper" describe "User views labels" do - set(:project) { create(:project_empty_repo, :public) } - set(:user) { create(:user) } - + let_it_be(:project) { create(:project_empty_repo, :public) } + let_it_be(:user) { create(:user) } let(:label_titles) { %w[bug enhancement feature] } let!(:prioritized_label) { create(:label, project: project, title: 'prioritized-label-name', priority: 1) } diff --git a/spec/features/projects/settings/project_settings_spec.rb b/spec/features/projects/settings/project_settings_spec.rb index b601866c96b..9fc91550667 100644 --- a/spec/features/projects/settings/project_settings_spec.rb +++ b/spec/features/projects/settings/project_settings_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'Projects settings' do - set(:project) { create(:project) } + let_it_be(:project) { create(:project) } let(:user) { project.owner } let(:panel) { find('.general-settings', match: :first) } let(:button) { panel.find('.btn.js-settings-toggle') } diff --git a/spec/features/projects/show/user_sees_git_instructions_spec.rb b/spec/features/projects/show/user_sees_git_instructions_spec.rb index dde9490a5e1..0c486056329 100644 --- a/spec/features/projects/show/user_sees_git_instructions_spec.rb +++ b/spec/features/projects/show/user_sees_git_instructions_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'Projects > Show > User sees Git instructions' do - set(:user) { create(:user) } + let_it_be(:user) { create(:user) } shared_examples_for 'redirects to the sign in page' do it 'redirects to the sign in page' do @@ -49,7 +49,7 @@ describe 'Projects > Show > User sees Git instructions' do context 'when project is public' do context 'when project has no repo' do - set(:project) { create(:project, :public) } + let_it_be(:project) { create(:project, :public) } before do sign_in(project.owner) @@ -60,7 +60,7 @@ describe 'Projects > Show > User sees Git instructions' do end context 'when project is empty' do - set(:project) { create(:project_empty_repo, :public) } + let_it_be(:project) { create(:project_empty_repo, :public) } context 'when not signed in' do before do @@ -98,7 +98,7 @@ describe 'Projects > Show > User sees Git instructions' do end context 'when project is not empty' do - set(:project) { create(:project, :public, :repository) } + let_it_be(:project) { create(:project, :public, :repository) } before do visit(project_path(project)) @@ -141,7 +141,7 @@ describe 'Projects > Show > User sees Git instructions' do end context 'when project is internal' do - set(:project) { create(:project, :internal, :repository) } + let_it_be(:project) { create(:project, :internal, :repository) } context 'when not signed in' do before do @@ -163,7 +163,7 @@ describe 'Projects > Show > User sees Git instructions' do end context 'when project is private' do - set(:project) { create(:project, :private) } + let_it_be(:project) { create(:project, :private) } before do visit(project_path(project)) diff --git a/spec/features/projects/show/user_sees_last_commit_ci_status_spec.rb b/spec/features/projects/show/user_sees_last_commit_ci_status_spec.rb index cf1a679102c..5aba16597b8 100644 --- a/spec/features/projects/show/user_sees_last_commit_ci_status_spec.rb +++ b/spec/features/projects/show/user_sees_last_commit_ci_status_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'Projects > Show > User sees last commit CI status' do - set(:project) { create(:project, :repository, :public) } + let_it_be(:project) { create(:project, :repository, :public) } it 'shows the project README', :js do project.enable_ci diff --git a/spec/features/projects/show/user_sees_readme_spec.rb b/spec/features/projects/show/user_sees_readme_spec.rb index 98906de4620..52745b06cd3 100644 --- a/spec/features/projects/show/user_sees_readme_spec.rb +++ b/spec/features/projects/show/user_sees_readme_spec.rb @@ -3,9 +3,8 @@ require 'spec_helper' describe 'Projects > Show > User sees README' do - set(:user) { create(:user) } - - set(:project) { create(:project, :repository, :public) } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :repository, :public) } it 'shows the project README', :js do visit project_path(project) diff --git a/spec/features/projects/user_sees_user_popover_spec.rb b/spec/features/projects/user_sees_user_popover_spec.rb index adbf9073d59..fafb3773866 100644 --- a/spec/features/projects/user_sees_user_popover_spec.rb +++ b/spec/features/projects/user_sees_user_popover_spec.rb @@ -3,8 +3,7 @@ require 'spec_helper' describe 'User sees user popover', :js do - set(:project) { create(:project, :repository) } - + let_it_be(:project) { create(:project, :repository) } let(:user) { project.creator } let(:merge_request) do create(:merge_request, source_project: project, target_project: project) diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb index 331ba58d067..7d18c0f7a14 100644 --- a/spec/features/projects/wiki/markdown_preview_spec.rb +++ b/spec/features/projects/wiki/markdown_preview_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'Projects > Wiki > User previews markdown changes', :js do - set(:user) { create(:user) } + let_it_be(:user) { create(:user) } let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: '[some link](other-page)' }) } let(:wiki_content) do diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb index 7503c8aa52e..e67982bbd31 100644 --- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb @@ -3,6 +3,8 @@ require "spec_helper" describe "User creates wiki page" do + include WikiHelpers + let(:user) { create(:user) } let(:wiki) { ProjectWiki.new(project, user) } let(:project) { create(:project) } @@ -14,9 +16,11 @@ describe "User creates wiki page" do end context "when wiki is empty" do - before do + before do |example| visit(project_wikis_path(project)) + wait_for_svg_to_be_loaded(example) + click_link "Create your first page" end @@ -45,7 +49,7 @@ describe "User creates wiki page" do expect(page).to have_content("Create New Page") end - it "shows non-escaped link in the pages list", :quarantine do + it "shows non-escaped link in the pages list" do fill_in(:wiki_title, with: "one/two/three-test") page.within(".wiki-form") do @@ -163,7 +167,7 @@ describe "User creates wiki page" do expect(page).to have_link('Link to Home', href: "/#{project.full_path}/-/wikis/home") end - it_behaves_like 'wiki file attachments', :quarantine + it_behaves_like 'wiki file attachments' end context "in a group namespace", :js do @@ -175,7 +179,7 @@ describe "User creates wiki page" do expect(page).to have_field("wiki[message]", with: "Create home") end - it "creates a page from the home page", :quarantine do + it "creates a page from the home page" do page.within(".wiki-form") do fill_in(:wiki_content, with: "My awesome wiki!") diff --git a/spec/features/projects/wiki/user_views_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_page_spec.rb index c7856342fb2..1a9cde4571e 100644 --- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb @@ -19,9 +19,12 @@ describe 'User views a wiki page' do sign_in(user) end - context 'when wiki is empty' do + context 'when wiki is empty', :js do before do - visit(project_wikis_path(project)) + visit project_wikis_path(project) + + wait_for_svg_to_be_loaded + click_link "Create your first page" fill_in(:wiki_title, with: 'one/two/three-test') @@ -32,7 +35,7 @@ describe 'User views a wiki page' do end end - it 'shows the history of a page that has a path', :js do + it 'shows the history of a page that has a path' do expect(current_path).to include('one/two/three-test') first(:link, text: 'three').click @@ -45,7 +48,7 @@ describe 'User views a wiki page' do end end - it 'shows an old version of a page', :js do + it 'shows an old version of a page' do expect(current_path).to include('one/two/three-test') expect(find('.wiki-pages')).to have_content('three') @@ -162,9 +165,12 @@ describe 'User views a wiki page' do end it 'opens a default wiki page', :js do - visit(project_path(project)) + visit project_path(project) find('.shortcuts-wiki').click + + wait_for_svg_to_be_loaded + click_link "Create your first page" expect(page).to have_content('Create New Page') diff --git a/spec/features/read_only_spec.rb b/spec/features/read_only_spec.rb index 619d34ebed4..a33535a7b0b 100644 --- a/spec/features/read_only_spec.rb +++ b/spec/features/read_only_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'read-only message' do - set(:user) { create(:user) } + let_it_be(:user) { create(:user) } before do sign_in(user) diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index ed1dbe15d65..45b57b5cb1b 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' describe "Internal Project Access" do include AccessMatchers - set(:project) { create(:project, :internal, :repository) } + let_it_be(:project, reload: true) { create(:project, :internal, :repository) } describe "Project should be internal" do describe '#internal?' do diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb index 97e6b3bd4ff..9aeb3ffbd43 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' describe "Private Project Access" do include AccessMatchers - set(:project) { create(:project, :private, :repository, public_builds: false) } + let_it_be(:project, reload: true) { create(:project, :private, :repository, public_builds: false) } describe "Project should be private" do describe '#private?' do diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index 24bbb8d9b9e..4d8c2c7822c 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' describe "Public Project Access" do include AccessMatchers - set(:project) { create(:project, :public, :repository) } + let_it_be(:project, reload: true) { create(:project, :public, :repository) } describe "Project should be public" do describe '#public?' do diff --git a/spec/features/user_sorts_things_spec.rb b/spec/features/user_sorts_things_spec.rb index 41f8f3761e8..8397854df27 100644 --- a/spec/features/user_sorts_things_spec.rb +++ b/spec/features/user_sorts_things_spec.rb @@ -10,10 +10,10 @@ describe "User sorts things" do include Spec::Support::Helpers::Features::SortingHelpers include DashboardHelper - set(:project) { create(:project_empty_repo, :public) } - set(:current_user) { create(:user) } # Using `current_user` instead of just `user` because of the hardoced call in `assigned_mrs_dashboard_path` which is used below. - set(:issue) { create(:issue, project: project, author: current_user) } - set(:merge_request) { create(:merge_request, target_project: project, source_project: project, author: current_user) } + let_it_be(:project) { create(:project_empty_repo, :public) } + let_it_be(:current_user) { create(:user) } # Using `current_user` instead of just `user` because of the hardoced call in `assigned_mrs_dashboard_path` which is used below. + let_it_be(:issue) { create(:issue, project: project, author: current_user) } + let_it_be(:merge_request) { create(:merge_request, target_project: project, source_project: project, author: current_user) } before do project.add_developer(current_user) diff --git a/spec/fixtures/trace/sample_trace b/spec/fixtures/trace/sample_trace index d774d154496..f768c038f9e 100644 --- a/spec/fixtures/trace/sample_trace +++ b/spec/fixtures/trace/sample_trace @@ -1442,7 +1442,7 @@ TodoService marks a single todo id as done caches the number of todos of a user -Gitlab::ImportExport::ProjectTreeSaver +Gitlab::ImportExport::Project::TreeSaver saves the project tree into a json object saves project successfully JSON diff --git a/spec/lib/gitlab/checks/branch_check_spec.rb b/spec/lib/gitlab/checks/branch_check_spec.rb index 7cc1722dfd4..fd7eaa1603f 100644 --- a/spec/lib/gitlab/checks/branch_check_spec.rb +++ b/spec/lib/gitlab/checks/branch_check_spec.rb @@ -15,7 +15,7 @@ describe Gitlab::Checks::BranchCheck do let(:ref) { 'refs/heads/master' } it 'raises an error' do - expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'The default branch of a project cannot be deleted.') + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'The default branch of a project cannot be deleted.') end end @@ -28,7 +28,7 @@ describe Gitlab::Checks::BranchCheck do it 'raises an error if the user is not allowed to do forced pushes to protected branches' do expect(Gitlab::Checks::ForcePush).to receive(:force_push?).and_return(true) - expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to force push code to a protected branch on this project.') + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You are not allowed to force push code to a protected branch on this project.') end it 'raises an error if the user is not allowed to merge to protected branches' do @@ -38,13 +38,13 @@ describe Gitlab::Checks::BranchCheck do expect(user_access).to receive(:can_merge_to_branch?).and_return(false) expect(user_access).to receive(:can_push_to_branch?).and_return(false) - expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to merge code into protected branches on this project.') + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You are not allowed to merge code into protected branches on this project.') end it 'raises an error if the user is not allowed to push to protected branches' do expect(user_access).to receive(:can_push_to_branch?).and_return(false) - expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to protected branches on this project.') + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You are not allowed to push code to protected branches on this project.') end context 'when project repository is empty' do @@ -58,7 +58,7 @@ describe Gitlab::Checks::BranchCheck do end it 'raises an error' do - expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /Ask a project Owner or Maintainer to create a default branch/) + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, /Ask a project Owner or Maintainer to create a default branch/) end end @@ -109,7 +109,7 @@ describe Gitlab::Checks::BranchCheck do end it 'raises an error' do - expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to create protected branches on this project.') + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You are not allowed to create protected branches on this project.') end end @@ -135,7 +135,7 @@ describe Gitlab::Checks::BranchCheck do end it 'raises an error' do - expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only use an existing protected branch ref as the basis of a new protected branch.') + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You can only use an existing protected branch ref as the basis of a new protected branch.') end end @@ -157,7 +157,7 @@ describe Gitlab::Checks::BranchCheck do context 'via SSH' do it 'raises an error' do - expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only create protected branches using the web interface and API.') + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You can only create protected branches using the web interface and API.') end end end @@ -171,7 +171,7 @@ describe Gitlab::Checks::BranchCheck do context 'if the user is not allowed to delete protected branches' do it 'raises an error' do - expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to delete protected branches from this project. Only a project maintainer or owner can delete a protected branch.') + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You are not allowed to delete protected branches from this project. Only a project maintainer or owner can delete a protected branch.') end end @@ -190,7 +190,7 @@ describe Gitlab::Checks::BranchCheck do context 'over SSH or HTTP' do it 'raises an error' do - expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only delete protected branches using the web interface.') + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You can only delete protected branches using the web interface.') end end end diff --git a/spec/lib/gitlab/checks/diff_check_spec.rb b/spec/lib/gitlab/checks/diff_check_spec.rb index b9134b8d6ab..467b4ed3a21 100644 --- a/spec/lib/gitlab/checks/diff_check_spec.rb +++ b/spec/lib/gitlab/checks/diff_check_spec.rb @@ -34,7 +34,7 @@ describe Gitlab::Checks::DiffCheck do context 'when change is sent by a different user' do it 'raises an error if the user is not allowed to update the file' do - expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "The path 'README' is locked in Git LFS by #{lock.user.name}") + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "The path 'README' is locked in Git LFS by #{lock.user.name}") end end diff --git a/spec/lib/gitlab/checks/lfs_check_spec.rb b/spec/lib/gitlab/checks/lfs_check_spec.rb index dad14e100a7..c86481d1abe 100644 --- a/spec/lib/gitlab/checks/lfs_check_spec.rb +++ b/spec/lib/gitlab/checks/lfs_check_spec.rb @@ -50,7 +50,7 @@ describe Gitlab::Checks::LfsCheck do end it 'fails if any LFS blobs are missing' do - expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /LFS objects are missing/) + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, /LFS objects are missing/) end it 'succeeds if LFS objects have already been uploaded' do diff --git a/spec/lib/gitlab/checks/push_check_spec.rb b/spec/lib/gitlab/checks/push_check_spec.rb index e1bd52d6c0b..857d71732fe 100644 --- a/spec/lib/gitlab/checks/push_check_spec.rb +++ b/spec/lib/gitlab/checks/push_check_spec.rb @@ -15,7 +15,7 @@ describe Gitlab::Checks::PushCheck do expect(user_access).to receive(:can_do_action?).with(:push_code).and_return(false) expect(project).to receive(:branch_allows_collaboration?).with(user_access.user, 'master').and_return(false) - expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to this project.') + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You are not allowed to push code to this project.') end end end diff --git a/spec/lib/gitlab/checks/tag_check_spec.rb b/spec/lib/gitlab/checks/tag_check_spec.rb index 80e9eb504ad..0c94171646e 100644 --- a/spec/lib/gitlab/checks/tag_check_spec.rb +++ b/spec/lib/gitlab/checks/tag_check_spec.rb @@ -11,7 +11,7 @@ describe Gitlab::Checks::TagCheck do it 'raises an error when user does not have access' do allow(user_access).to receive(:can_do_action?).with(:admin_tag).and_return(false) - expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to change existing tags on this project.') + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You are not allowed to change existing tags on this project.') end context 'with protected tag' do @@ -27,7 +27,7 @@ describe Gitlab::Checks::TagCheck do let(:newrev) { '0000000000000000000000000000000000000000' } it 'is prevented' do - expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /cannot be deleted/) + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, /cannot be deleted/) end end @@ -36,7 +36,7 @@ describe Gitlab::Checks::TagCheck do let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' } it 'is prevented' do - expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /cannot be updated/) + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, /cannot be updated/) end end end @@ -47,7 +47,7 @@ describe Gitlab::Checks::TagCheck do let(:ref) { 'refs/tags/v9.1.0' } it 'prevents creation below access level' do - expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /allowed to create this tag as it is protected/) + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, /allowed to create this tag as it is protected/) end context 'when user has access' do diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 0831021b22b..f95349a2125 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -32,8 +32,8 @@ describe Gitlab::GitAccess do it 'blocks ssh git push and pull' do aggregate_failures do - expect { push_access_check }.to raise_unauthorized('Git access over SSH is not allowed') - expect { pull_access_check }.to raise_unauthorized('Git access over SSH is not allowed') + expect { push_access_check }.to raise_forbidden('Git access over SSH is not allowed') + expect { pull_access_check }.to raise_forbidden('Git access over SSH is not allowed') end end end @@ -48,8 +48,8 @@ describe Gitlab::GitAccess do it 'blocks http push and pull' do aggregate_failures do - expect { push_access_check }.to raise_unauthorized('Git access over HTTP is not allowed') - expect { pull_access_check }.to raise_unauthorized('Git access over HTTP is not allowed') + expect { push_access_check }.to raise_forbidden('Git access over HTTP is not allowed') + expect { pull_access_check }.to raise_forbidden('Git access over HTTP is not allowed') end end @@ -58,7 +58,7 @@ describe Gitlab::GitAccess do it "doesn't block http pull" do aggregate_failures do - expect { pull_access_check }.not_to raise_unauthorized('Git access over HTTP is not allowed') + expect { pull_access_check }.not_to raise_forbidden('Git access over HTTP is not allowed') end end @@ -67,7 +67,7 @@ describe Gitlab::GitAccess do it "doesn't block http pull" do aggregate_failures do - expect { pull_access_check }.not_to raise_unauthorized('Git access over HTTP is not allowed') + expect { pull_access_check }.not_to raise_forbidden('Git access over HTTP is not allowed') end end end @@ -165,7 +165,7 @@ describe Gitlab::GitAccess do end it 'does not block pushes with "not found"' do - expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload]) + expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:auth_upload]) end end @@ -178,7 +178,7 @@ describe Gitlab::GitAccess do end it 'blocks the push' do - expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:upload]) + expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:upload]) end end @@ -208,7 +208,7 @@ describe Gitlab::GitAccess do end it 'does not block pushes with "not found"' do - expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:upload]) + expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:upload]) end end @@ -285,8 +285,8 @@ describe Gitlab::GitAccess do it 'does not allow keys which are too small', :aggregate_failures do expect(actor).not_to be_valid - expect { pull_access_check }.to raise_unauthorized('Your SSH key must be at least 4096 bits.') - expect { push_access_check }.to raise_unauthorized('Your SSH key must be at least 4096 bits.') + expect { pull_access_check }.to raise_forbidden('Your SSH key must be at least 4096 bits.') + expect { push_access_check }.to raise_forbidden('Your SSH key must be at least 4096 bits.') end end @@ -297,8 +297,8 @@ describe Gitlab::GitAccess do it 'does not allow keys which are too small', :aggregate_failures do expect(actor).not_to be_valid - expect { pull_access_check }.to raise_unauthorized(/Your SSH key type is forbidden/) - expect { push_access_check }.to raise_unauthorized(/Your SSH key type is forbidden/) + expect { pull_access_check }.to raise_forbidden(/Your SSH key type is forbidden/) + expect { push_access_check }.to raise_forbidden(/Your SSH key type is forbidden/) end end end @@ -363,7 +363,7 @@ describe Gitlab::GitAccess do let(:authentication_abilities) { [] } it 'raises unauthorized with download error' do - expect { pull_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_download]) + expect { pull_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:auth_download]) end context 'when authentication abilities include download code' do @@ -387,7 +387,7 @@ describe Gitlab::GitAccess do let(:authentication_abilities) { [] } it 'raises unauthorized with push error' do - expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload]) + expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:auth_upload]) end context 'when authentication abilities include push code' do @@ -414,7 +414,7 @@ describe Gitlab::GitAccess do end context 'when calling git-upload-pack' do - it { expect { pull_access_check }.to raise_unauthorized('Pulling over HTTP is not allowed.') } + it { expect { pull_access_check }.to raise_forbidden('Pulling over HTTP is not allowed.') } end context 'when calling git-receive-pack' do @@ -428,7 +428,7 @@ describe Gitlab::GitAccess do end context 'when calling git-receive-pack' do - it { expect { push_access_check }.to raise_unauthorized('Pushing over HTTP is not allowed.') } + it { expect { push_access_check }.to raise_forbidden('Pushing over HTTP is not allowed.') } end context 'when calling git-upload-pack' do @@ -445,7 +445,7 @@ describe Gitlab::GitAccess do allow(Gitlab::Database).to receive(:read_only?) { true } end - it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:cannot_push_to_read_only]) } + it { expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:cannot_push_to_read_only]) } end end @@ -559,21 +559,21 @@ describe Gitlab::GitAccess do it 'disallows guests to pull' do project.add_guest(user) - expect { pull_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:download]) + expect { pull_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:download]) end it 'disallows blocked users to pull' do project.add_maintainer(user) user.block - expect { pull_access_check }.to raise_unauthorized('Your account has been blocked.') + expect { pull_access_check }.to raise_forbidden('Your account has been blocked.') end it 'disallows deactivated users to pull' do project.add_maintainer(user) user.deactivate! - expect { pull_access_check }.to raise_unauthorized("Your account has been deactivated by your administrator. Please log back in from a web browser to reactivate your account at #{Gitlab.config.gitlab.url}") + expect { pull_access_check }.to raise_forbidden("Your account has been deactivated by your administrator. Please log back in from a web browser to reactivate your account at #{Gitlab.config.gitlab.url}") end context 'when the project repository does not exist' do @@ -610,7 +610,7 @@ describe Gitlab::GitAccess do it 'does not give access to download code' do public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED) - expect { pull_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:download]) + expect { pull_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:download]) end end end @@ -722,7 +722,7 @@ describe Gitlab::GitAccess do context 'when is not member of the project' do context 'pull code' do - it { expect { pull_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:download]) } + it { expect { pull_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:download]) } end end end @@ -828,7 +828,7 @@ describe Gitlab::GitAccess do expect(&check).not_to raise_error, -> { "expected #{action} to be allowed" } else - expect(&check).to raise_error(Gitlab::GitAccess::UnauthorizedError), + expect(&check).to raise_error(Gitlab::GitAccess::ForbiddenError), -> { "expected #{action} to be disallowed" } end end @@ -965,7 +965,7 @@ describe Gitlab::GitAccess do it 'does not allow deactivated users to push' do user.deactivate! - expect { push_access_check }.to raise_unauthorized("Your account has been deactivated by your administrator. Please log back in from a web browser to reactivate your account at #{Gitlab.config.gitlab.url}") + expect { push_access_check }.to raise_forbidden("Your account has been deactivated by your administrator. Please log back in from a web browser to reactivate your account at #{Gitlab.config.gitlab.url}") end it 'cleans up the files' do @@ -1009,26 +1009,26 @@ describe Gitlab::GitAccess do project.add_reporter(user) end - it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload]) } + it { expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:auth_upload]) } end context 'when unauthorized' do context 'to public project' do let(:project) { create(:project, :public, :repository) } - it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload]) } + it { expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:auth_upload]) } end context 'to internal project' do let(:project) { create(:project, :internal, :repository) } - it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload]) } + it { expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:auth_upload]) } end context 'to private project' do let(:project) { create(:project, :private, :repository) } - it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload]) } + it { expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:auth_upload]) } end end end @@ -1039,7 +1039,7 @@ describe Gitlab::GitAccess do it 'denies push access' do project.add_maintainer(user) - expect { push_access_check }.to raise_unauthorized('The repository is temporarily read-only. Please try again later.') + expect { push_access_check }.to raise_forbidden('The repository is temporarily read-only. Please try again later.') end end @@ -1060,7 +1060,7 @@ describe Gitlab::GitAccess do context 'to public project' do let(:project) { create(:project, :public, :repository) } - it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:deploy_key_upload]) } + it { expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:deploy_key_upload]) } end context 'to internal project' do @@ -1083,14 +1083,14 @@ describe Gitlab::GitAccess do key.deploy_keys_projects.create(project: project, can_push: false) end - it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:deploy_key_upload]) } + it { expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:deploy_key_upload]) } end context 'when unauthorized' do context 'to public project' do let(:project) { create(:project, :public, :repository) } - it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:deploy_key_upload]) } + it { expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:deploy_key_upload]) } end context 'to internal project' do @@ -1121,7 +1121,7 @@ describe Gitlab::GitAccess do it 'blocks access when the user did not accept terms', :aggregate_failures do actions.each do |action| - expect { action.call }.to raise_unauthorized(/must accept the Terms of Service in order to perform this action/) + expect { action.call }.to raise_forbidden(/must accept the Terms of Service in order to perform this action/) end end @@ -1211,8 +1211,8 @@ describe Gitlab::GitAccess do access.check('git-receive-pack', changes) end - def raise_unauthorized(message) - raise_error(Gitlab::GitAccess::UnauthorizedError, message) + def raise_forbidden(message) + raise_error(Gitlab::GitAccess::ForbiddenError, message) end def raise_not_found diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb index 99c9369a2b9..b5e673c9e79 100644 --- a/spec/lib/gitlab/git_access_wiki_spec.rb +++ b/spec/lib/gitlab/git_access_wiki_spec.rb @@ -33,7 +33,7 @@ describe Gitlab::GitAccessWiki do end it 'does not give access to upload wiki code' do - expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "You can't push code to a read-only GitLab instance.") + expect { subject }.to raise_error(Gitlab::GitAccess::ForbiddenError, "You can't push code to a read-only GitLab instance.") end end end @@ -70,7 +70,7 @@ describe Gitlab::GitAccessWiki do it 'does not give access to download wiki code' do project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED) - expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to download code from this project.') + expect { subject }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You are not allowed to download code from this project.') end end end diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index 5c36d6d35af..00182983418 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -306,5 +306,19 @@ describe Gitlab::GitalyClient::CommitService do client.find_commits(order: 'topo') end + + it 'sends an RPC request with an author' do + request = Gitaly::FindCommitsRequest.new( + repository: repository_message, + disable_walk: true, + order: 'NONE', + author: "Billy Baggins <bilbo@shire.com>" + ) + + expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:find_commits) + .with(request, kind_of(Hash)).and_return([]) + + client.find_commits(order: 'default', author: "Billy Baggins <bilbo@shire.com>") + end end end diff --git a/spec/lib/gitlab/import_export/base_object_builder_spec.rb b/spec/lib/gitlab/import_export/base/object_builder_spec.rb index fbb3b08cf56..e5242ae0bfc 100644 --- a/spec/lib/gitlab/import_export/base_object_builder_spec.rb +++ b/spec/lib/gitlab/import_export/base/object_builder_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Gitlab::ImportExport::BaseObjectBuilder do +describe Gitlab::ImportExport::Base::ObjectBuilder do let(:project) do create(:project, :repository, :builds_disabled, @@ -11,7 +11,7 @@ describe Gitlab::ImportExport::BaseObjectBuilder do path: 'project') end let(:klass) { Milestone } - let(:attributes) { { 'title' => 'Test BaseObjectBuilder Milestone', 'project' => project } } + let(:attributes) { { 'title' => 'Test Base::ObjectBuilder Milestone', 'project' => project } } subject { described_class.build(klass, attributes) } diff --git a/spec/lib/gitlab/import_export/base_relation_factory_spec.rb b/spec/lib/gitlab/import_export/base/relation_factory_spec.rb index e02d8f3d2e6..1011de83c95 100644 --- a/spec/lib/gitlab/import_export/base_relation_factory_spec.rb +++ b/spec/lib/gitlab/import_export/base/relation_factory_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Gitlab::ImportExport::BaseRelationFactory do +describe Gitlab::ImportExport::Base::RelationFactory do let(:user) { create(:admin) } let(:project) { create(:project) } let(:members_mapper) { double('members_mapper').as_null_object } @@ -13,7 +13,7 @@ describe Gitlab::ImportExport::BaseRelationFactory do subject do described_class.create(relation_sym: relation_sym, relation_hash: relation_hash, - object_builder: Gitlab::ImportExport::GroupProjectObjectBuilder, + object_builder: Gitlab::ImportExport::Project::ObjectBuilder, members_mapper: members_mapper, user: user, importable: project, diff --git a/spec/lib/gitlab/import_export/fork_spec.rb b/spec/lib/gitlab/import_export/fork_spec.rb index 09e4f62c686..8aa28353c04 100644 --- a/spec/lib/gitlab/import_export/fork_spec.rb +++ b/spec/lib/gitlab/import_export/fork_spec.rb @@ -24,11 +24,11 @@ describe 'forked project import' do end let(:saver) do - Gitlab::ImportExport::ProjectTreeSaver.new(project: project_with_repo, current_user: user, shared: shared) + Gitlab::ImportExport::Project::TreeSaver.new(project: project_with_repo, current_user: user, shared: shared) end let(:restorer) do - Gitlab::ImportExport::ProjectTreeRestorer.new(user: user, shared: shared, project: project) + Gitlab::ImportExport::Project::TreeRestorer.new(user: user, shared: shared, project: project) end before do diff --git a/spec/lib/gitlab/import_export/group_object_builder_spec.rb b/spec/lib/gitlab/import_export/group/object_builder_spec.rb index 08b2dae1147..781670b0aa5 100644 --- a/spec/lib/gitlab/import_export/group_object_builder_spec.rb +++ b/spec/lib/gitlab/import_export/group/object_builder_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Gitlab::ImportExport::GroupObjectBuilder do +describe Gitlab::ImportExport::Group::ObjectBuilder do let(:group) { create(:group) } let(:base_attributes) do { diff --git a/spec/lib/gitlab/import_export/group_relation_factory_spec.rb b/spec/lib/gitlab/import_export/group/relation_factory_spec.rb index 9208b2ad203..332648d5c89 100644 --- a/spec/lib/gitlab/import_export/group_relation_factory_spec.rb +++ b/spec/lib/gitlab/import_export/group/relation_factory_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Gitlab::ImportExport::GroupRelationFactory do +describe Gitlab::ImportExport::Group::RelationFactory do let(:group) { create(:group) } let(:members_mapper) { double('members_mapper').as_null_object } let(:user) { create(:admin) } @@ -11,7 +11,7 @@ describe Gitlab::ImportExport::GroupRelationFactory do described_class.create(relation_sym: relation_sym, relation_hash: relation_hash, members_mapper: members_mapper, - object_builder: Gitlab::ImportExport::GroupObjectBuilder, + object_builder: Gitlab::ImportExport::Group::ObjectBuilder, user: user, importable: group, excluded_keys: excluded_keys) diff --git a/spec/lib/gitlab/import_export/group_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb index b2c8398d358..5584f1503f7 100644 --- a/spec/lib/gitlab/import_export/group_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Gitlab::ImportExport::GroupTreeRestorer do +describe Gitlab::ImportExport::Group::TreeRestorer do include ImportExport::CommonUtil let(:shared) { Gitlab::ImportExport::Shared.new(group) } diff --git a/spec/lib/gitlab/import_export/group_tree_saver_spec.rb b/spec/lib/gitlab/import_export/group/tree_saver_spec.rb index 7f49c7af8fa..845eb8e308b 100644 --- a/spec/lib/gitlab/import_export/group_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/group/tree_saver_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Gitlab::ImportExport::GroupTreeSaver do +describe Gitlab::ImportExport::Group::TreeSaver do describe 'saves the group tree into a json object' do let(:shared) { Gitlab::ImportExport::Shared.new(group) } let(:group_tree_saver) { described_class.new(group: group, current_user: user, shared: shared) } @@ -72,7 +72,7 @@ describe Gitlab::ImportExport::GroupTreeSaver do # except: # context 'with description override' do # context 'group members' do - # ^ These are specific for the groupTreeSaver + # ^ These are specific for the Group::TreeSaver context 'JSON' do let(:saved_group_json) do group_tree_saver.save diff --git a/spec/lib/gitlab/import_export/importer_spec.rb b/spec/lib/gitlab/import_export/importer_spec.rb index 942af4084e5..07857269004 100644 --- a/spec/lib/gitlab/import_export/importer_spec.rb +++ b/spec/lib/gitlab/import_export/importer_spec.rb @@ -63,7 +63,7 @@ describe Gitlab::ImportExport::Importer do end it 'restores the ProjectTree' do - expect(Gitlab::ImportExport::ProjectTreeRestorer).to receive(:new).and_call_original + expect(Gitlab::ImportExport::Project::TreeRestorer).to receive(:new).and_call_original importer.execute end diff --git a/spec/lib/gitlab/import_export/group_project_object_builder_spec.rb b/spec/lib/gitlab/import_export/project/object_builder_spec.rb index 34049cbf570..c9d1410400a 100644 --- a/spec/lib/gitlab/import_export/group_project_object_builder_spec.rb +++ b/spec/lib/gitlab/import_export/project/object_builder_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Gitlab::ImportExport::GroupProjectObjectBuilder do +describe Gitlab::ImportExport::Project::ObjectBuilder do let!(:group) { create(:group, :private) } let!(:subgroup) { create(:group, :private, parent: group) } let!(:project) do diff --git a/spec/lib/gitlab/import_export/project_relation_factory_spec.rb b/spec/lib/gitlab/import_export/project/relation_factory_spec.rb index d0e89b2e57b..73ae6810706 100644 --- a/spec/lib/gitlab/import_export/project_relation_factory_spec.rb +++ b/spec/lib/gitlab/import_export/project/relation_factory_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Gitlab::ImportExport::ProjectRelationFactory do +describe Gitlab::ImportExport::Project::RelationFactory do let(:group) { create(:group) } let(:project) { create(:project, :repository, group: group) } let(:members_mapper) { double('members_mapper').as_null_object } @@ -11,7 +11,7 @@ describe Gitlab::ImportExport::ProjectRelationFactory do let(:created_object) do described_class.create(relation_sym: relation_sym, relation_hash: relation_hash, - object_builder: Gitlab::ImportExport::GroupProjectObjectBuilder, + object_builder: Gitlab::ImportExport::Project::ObjectBuilder, members_mapper: members_mapper, user: user, importable: project, @@ -243,11 +243,11 @@ describe Gitlab::ImportExport::ProjectRelationFactory do context 'Project references' do let(:relation_sym) { :project_foo_model } let(:relation_hash) do - Gitlab::ImportExport::ProjectRelationFactory::PROJECT_REFERENCES.map { |ref| { ref => 99 } }.inject(:merge) + Gitlab::ImportExport::Project::RelationFactory::PROJECT_REFERENCES.map { |ref| { ref => 99 } }.inject(:merge) end class ProjectFooModel < FooModel - attr_accessor(*Gitlab::ImportExport::ProjectRelationFactory::PROJECT_REFERENCES) + attr_accessor(*Gitlab::ImportExport::Project::RelationFactory::PROJECT_REFERENCES) end before do diff --git a/spec/lib/gitlab/import_export/project_tree_loader_spec.rb b/spec/lib/gitlab/import_export/project/tree_loader_spec.rb index b22de5a3f7b..e683eefa7c0 100644 --- a/spec/lib/gitlab/import_export/project_tree_loader_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_loader_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Gitlab::ImportExport::ProjectTreeLoader do +describe Gitlab::ImportExport::Project::TreeLoader do let(:fixture) { 'spec/fixtures/lib/gitlab/import_export/with_duplicates.json' } let(:project_tree) { JSON.parse(File.read(fixture)) } diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb index c899217d164..312bbb58a28 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true require 'spec_helper' -include ImportExport::CommonUtil -describe Gitlab::ImportExport::ProjectTreeRestorer do +describe Gitlab::ImportExport::Project::TreeRestorer do include ImportExport::CommonUtil let(:shared) { project.import_export_shared } diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb index 126ac289a56..151fdf8810f 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Gitlab::ImportExport::ProjectTreeSaver do +describe Gitlab::ImportExport::Project::TreeSaver do describe 'saves the project tree into a json object' do let(:shared) { project.import_export_shared } let(:project_tree_saver) { described_class.new(project: project, current_user: user, shared: shared) } @@ -75,7 +75,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver do # except: # context 'with description override' do # context 'group members' do - # ^ These are specific for the ProjectTreeSaver + # ^ These are specific for the Project::TreeSaver context 'JSON' do let(:saved_project_json) do project_tree_saver.save diff --git a/spec/lib/gitlab/import_export/relation_rename_service_spec.rb b/spec/lib/gitlab/import_export/relation_rename_service_spec.rb index d62f5725f9e..2e251154e9f 100644 --- a/spec/lib/gitlab/import_export/relation_rename_service_spec.rb +++ b/spec/lib/gitlab/import_export/relation_rename_service_spec.rb @@ -22,7 +22,7 @@ describe Gitlab::ImportExport::RelationRenameService do end context 'when importing' do - let(:project_tree_restorer) { Gitlab::ImportExport::ProjectTreeRestorer.new(user: user, shared: shared, project: project) } + let(:project_tree_restorer) { Gitlab::ImportExport::Project::TreeRestorer.new(user: user, shared: shared, project: project) } let(:file_content) { IO.read(File.join(shared.export_path, 'project.json')) } let(:json_file) { ActiveSupport::JSON.decode(file_content) } @@ -99,7 +99,7 @@ describe Gitlab::ImportExport::RelationRenameService do let(:relation_tree_saver) { Gitlab::ImportExport::RelationTreeSaver.new } let(:project_tree_saver) do - Gitlab::ImportExport::ProjectTreeSaver.new( + Gitlab::ImportExport::Project::TreeSaver.new( project: project, current_user: user, shared: shared) end diff --git a/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb index edb2c0a131a..80901feb893 100644 --- a/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb @@ -39,8 +39,8 @@ describe Gitlab::ImportExport::RelationTreeRestorer do context 'when restoring a project' do let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/project.json' } let(:importable) { create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') } - let(:object_builder) { Gitlab::ImportExport::GroupProjectObjectBuilder } - let(:relation_factory) { Gitlab::ImportExport::ProjectRelationFactory } + let(:object_builder) { Gitlab::ImportExport::Project::ObjectBuilder } + let(:relation_factory) { Gitlab::ImportExport::Project::RelationFactory } let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) } let(:tree_hash) { importable_hash } diff --git a/spec/lib/gitlab_danger_spec.rb b/spec/lib/gitlab_danger_spec.rb index 26bf5d76756..18321541221 100644 --- a/spec/lib/gitlab_danger_spec.rb +++ b/spec/lib/gitlab_danger_spec.rb @@ -9,7 +9,7 @@ describe GitlabDanger do describe '.local_warning_message' do it 'returns an informational message with rules that can run' do - expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changes_size, gemfile, documentation, frozen_string, duplicate_yarn_dependencies, prettier, eslint, database, commit_messages') + expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changes_size, documentation, frozen_string, duplicate_yarn_dependencies, prettier, eslint, database, commit_messages') end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 00ffc3cae54..076897e6312 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -320,6 +320,21 @@ describe Repository do end end + context "when 'author' is set" do + it "returns commits from that author" do + commit = repository.commits(nil, limit: 1).first + known_author = "#{commit.author_name} <#{commit.author_email}>" + + expect(repository.commits(nil, author: known_author, limit: 1)).not_to be_empty + end + + it "doesn't returns commits from an unknown author" do + unknown_author = "The Man With No Name <zapp@brannigan.com>" + + expect(repository.commits(nil, author: unknown_author, limit: 1)).to be_empty + end + end + context "when 'all' flag is set" do it 'returns every commit from the repository' do expect(repository.commits(nil, all: true, limit: 60).size).to eq(60) diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb index c3b5f9ded21..4249ce105c9 100644 --- a/spec/requests/api/internal/base_spec.rb +++ b/spec/requests/api/internal/base_spec.rb @@ -409,7 +409,7 @@ describe API::Internal::Base do it do pull(key, project) - expect(response).to have_gitlab_http_status(:unauthorized) + expect(response).to have_gitlab_http_status(:forbidden) expect(json_response["status"]).to be_falsey expect(user.reload.last_activity_on).to be_nil end @@ -419,7 +419,7 @@ describe API::Internal::Base do it do push(key, project) - expect(response).to have_gitlab_http_status(:unauthorized) + expect(response).to have_gitlab_http_status(:forbidden) expect(json_response["status"]).to be_falsey expect(user.reload.last_activity_on).to be_nil end @@ -518,7 +518,7 @@ describe API::Internal::Base do it do pull(key, personal_project) - expect(response).to have_gitlab_http_status(:unauthorized) + expect(response).to have_gitlab_http_status(:forbidden) expect(json_response["status"]).to be_falsey expect(user.reload.last_activity_on).to be_nil end @@ -528,7 +528,7 @@ describe API::Internal::Base do it do push(key, personal_project) - expect(response).to have_gitlab_http_status(:unauthorized) + expect(response).to have_gitlab_http_status(:forbidden) expect(json_response["status"]).to be_falsey expect(user.reload.last_activity_on).to be_nil end @@ -572,7 +572,7 @@ describe API::Internal::Base do it do push(key, project) - expect(response).to have_gitlab_http_status(:unauthorized) + expect(response).to have_gitlab_http_status(:forbidden) expect(json_response["status"]).to be_falsey end end @@ -654,7 +654,7 @@ describe API::Internal::Base do it 'rejects the SSH push' do push(key, project) - expect(response.status).to eq(401) + expect(response).to have_gitlab_http_status(:forbidden) expect(json_response['status']).to be_falsey expect(json_response['message']).to eq 'Git access over SSH is not allowed' end @@ -662,7 +662,7 @@ describe API::Internal::Base do it 'rejects the SSH pull' do pull(key, project) - expect(response.status).to eq(401) + expect(response).to have_gitlab_http_status(:forbidden) expect(json_response['status']).to be_falsey expect(json_response['message']).to eq 'Git access over SSH is not allowed' end @@ -676,7 +676,7 @@ describe API::Internal::Base do it 'rejects the HTTP push' do push(key, project, 'http') - expect(response.status).to eq(401) + expect(response).to have_gitlab_http_status(:forbidden) expect(json_response['status']).to be_falsey expect(json_response['message']).to eq 'Git access over HTTP is not allowed' end @@ -684,7 +684,7 @@ describe API::Internal::Base do it 'rejects the HTTP pull' do pull(key, project, 'http') - expect(response.status).to eq(401) + expect(response).to have_gitlab_http_status(:forbidden) expect(json_response['status']).to be_falsey expect(json_response['message']).to eq 'Git access over HTTP is not allowed' end diff --git a/spec/services/groups/import_export/export_service_spec.rb b/spec/services/groups/import_export/export_service_spec.rb index b1f76964722..5eebf08892a 100644 --- a/spec/services/groups/import_export/export_service_spec.rb +++ b/spec/services/groups/import_export/export_service_spec.rb @@ -7,7 +7,7 @@ describe Groups::ImportExport::ExportService do let!(:user) { create(:user) } let(:group) { create(:group) } let(:shared) { Gitlab::ImportExport::Shared.new(group) } - let(:export_path) { shared.export_path } + let(:archive_path) { shared.archive_path } let(:service) { described_class.new(group: group, user: user, params: { shared: shared }) } before do @@ -15,11 +15,11 @@ describe Groups::ImportExport::ExportService do end after do - FileUtils.rm_rf(export_path) + FileUtils.rm_rf(archive_path) end it 'saves the models' do - expect(Gitlab::ImportExport::GroupTreeSaver).to receive(:new).and_call_original + expect(Gitlab::ImportExport::Group::TreeSaver).to receive(:new).and_call_original service.execute end @@ -29,7 +29,7 @@ describe Groups::ImportExport::ExportService do service.execute expect(group.import_export_upload.export_file.file).not_to be_nil - expect(File.directory?(export_path)).to eq(false) + expect(File.directory?(archive_path)).to eq(false) expect(File.exist?(shared.archive_path)).to eq(false) end end @@ -46,25 +46,42 @@ describe Groups::ImportExport::ExportService do end end - context 'when saving services fail' do - before do - allow(service).to receive_message_chain(:tree_exporter, :save).and_return(false) + 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 - it 'removes the remaining exported data' do - allow_any_instance_of(Gitlab::ImportExport::Saver).to receive(:compress_and_save).and_return(false) + context 'when file compression fails' do + before do + allow(service).to receive_message_chain(:tree_exporter, :save).and_return(false) + end - expect { service.execute }.to raise_error(Gitlab::ImportExport::Error) + it 'removes the remaining exported data' do + allow_next_instance_of(Gitlab::ImportExport::Saver) do |saver| + allow(saver).to receive(:compress_and_save).and_return(false) + end - expect(group.import_export_upload).to be_nil - expect(File.directory?(export_path)).to eq(false) - expect(File.exist?(shared.archive_path)).to eq(false) - end + 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 - it 'notifies logger' do - expect_any_instance_of(Gitlab::Import::Logger).to receive(:error) + it 'notifies logger' do + 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 diff --git a/spec/services/projects/import_export/export_service_spec.rb b/spec/services/projects/import_export/export_service_spec.rb index 906fef6edf5..ec1771e64c2 100644 --- a/spec/services/projects/import_export/export_service_spec.rb +++ b/spec/services/projects/import_export/export_service_spec.rb @@ -27,7 +27,7 @@ describe Projects::ImportExport::ExportService do end it 'saves the models' do - expect(Gitlab::ImportExport::ProjectTreeSaver).to receive(:new).and_call_original + expect(Gitlab::ImportExport::Project::TreeSaver).to receive(:new).and_call_original service.execute end @@ -91,10 +91,10 @@ describe Projects::ImportExport::ExportService do end it 'removes the remaining exported data' do - allow(shared).to receive(:export_path).and_return('whatever') + allow(shared).to receive(:archive_path).and_return('whatever') allow(FileUtils).to receive(:rm_rf) - expect(FileUtils).to receive(:rm_rf).with(shared.export_path) + expect(FileUtils).to receive(:rm_rf).with(shared.archive_path) end it 'notifies the user' do @@ -121,10 +121,10 @@ describe Projects::ImportExport::ExportService do end it 'removes the remaining exported data' do - allow(shared).to receive(:export_path).and_return('whatever') + allow(shared).to receive(:archive_path).and_return('whatever') allow(FileUtils).to receive(:rm_rf) - expect(FileUtils).to receive(:rm_rf).with(shared.export_path) + expect(FileUtils).to receive(:rm_rf).with(shared.archive_path) end it 'notifies the user' do @@ -142,6 +142,21 @@ describe Projects::ImportExport::ExportService do end end + context 'when one of the savers fail unexpectedly' do + let(:archive_path) { shared.archive_path } + + before do + allow(service).to receive_message_chain(:uploads_saver, :save).and_return(false) + end + + it 'removes the remaining exported data' do + expect { service.execute }.to raise_error(Gitlab::ImportExport::Error) + + expect(project.import_export_upload).to be_nil + expect(File.exist?(shared.archive_path)).to eq(false) + end + end + context 'when user does not have admin_project permission' do let!(:another_user) { create(:user) } diff --git a/spec/support/helpers/wiki_helpers.rb b/spec/support/helpers/wiki_helpers.rb index 06cea728b42..86eb1793707 100644 --- a/spec/support/helpers/wiki_helpers.rb +++ b/spec/support/helpers/wiki_helpers.rb @@ -3,6 +3,11 @@ module WikiHelpers extend self + def wait_for_svg_to_be_loaded(example = nil) + # Ensure the SVG is loaded first before clicking the button + find('.svg-content .js-lazy-loaded') if example.nil? || example.metadata.key?(:js) + end + def upload_file_to_wiki(project, user, file_name) opts = { file_name: file_name, diff --git a/spec/support/import_export/common_util.rb b/spec/support/import_export/common_util.rb index 912a8e0a2ab..9281937e4ba 100644 --- a/spec/support/import_export/common_util.rb +++ b/spec/support/import_export/common_util.rb @@ -34,13 +34,13 @@ module ImportExport end def get_project_restorer(project, import_path) - Gitlab::ImportExport::ProjectTreeRestorer.new( + Gitlab::ImportExport::Project::TreeRestorer.new( user: project.creator, shared: get_shared_env(path: import_path), project: project ) end def get_project_saver(project, export_path) - Gitlab::ImportExport::ProjectTreeSaver.new( + Gitlab::ImportExport::Project::TreeSaver.new( project: project, current_user: project.creator, shared: get_shared_env(path: export_path) ) end diff --git a/spec/support/import_export/configuration_helper.rb b/spec/support/import_export/configuration_helper.rb index 27819b5201a..4fe619225bb 100644 --- a/spec/support/import_export/configuration_helper.rb +++ b/spec/support/import_export/configuration_helper.rb @@ -36,8 +36,8 @@ module ConfigurationHelper end def relation_class_for_name(relation_name) - relation_name = Gitlab::ImportExport::ProjectRelationFactory.overrides[relation_name.to_sym] || relation_name - Gitlab::ImportExport::ProjectRelationFactory.relation_class(relation_name) + relation_name = Gitlab::ImportExport::Project::RelationFactory.overrides[relation_name.to_sym] || relation_name + Gitlab::ImportExport::Project::RelationFactory.relation_class(relation_name) end def parsed_attributes(relation_name, attributes, config: Gitlab::ImportExport.config_file) diff --git a/spec/support/shared_examples/features/wiki_file_attachments_shared_examples.rb b/spec/support/shared_examples/features/wiki_file_attachments_shared_examples.rb index 36d91d323b5..867290fb2d6 100644 --- a/spec/support/shared_examples/features/wiki_file_attachments_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki_file_attachments_shared_examples.rb @@ -42,7 +42,7 @@ RSpec.shared_examples 'wiki file attachments' do end end - context 'uploading is complete', :quarantine do + context 'uploading is complete' do it 'shows "Attach a file" button on uploading complete' do attach_with_dropzone wait_for_requests diff --git a/spec/support/shared_examples/lib/gitlab/import_export/project_tree_restorer_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/import_export/project_tree_restorer_shared_examples.rb index 968423176f1..14af98285fc 100644 --- a/spec/support/shared_examples/lib/gitlab/import_export/project_tree_restorer_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/import_export/project_tree_restorer_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Shared examples for ProjectTreeRestorer (shared to allow the testing +# Shared examples for Project::TreeRestorer (shared to allow the testing # of EE-specific features) RSpec.shared_examples 'restores project successfully' do |**results| it 'restores the project' do |