Commit f89c3101 authored by Stan Hu's avatar Stan Hu

Merge branch 'ce-to-ee-2018-04-06' into 'master'

CE upstream - 2018-04-06 15:28 UTC

See merge request gitlab-org/gitlab-ee!5270
parents f349bdde 2c1510ff
...@@ -107,7 +107,6 @@ ...@@ -107,7 +107,6 @@
} }
} }
.commits-compare-switch { .commits-compare-switch {
float: left; float: left;
margin-right: 9px; margin-right: 9px;
...@@ -179,7 +178,7 @@ ...@@ -179,7 +178,7 @@
.commit-detail { .commit-detail {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: center;
flex-grow: 1; flex-grow: 1;
.merge-request-branches & { .merge-request-branches & {
...@@ -204,37 +203,63 @@ ...@@ -204,37 +203,63 @@
} }
.ci-status-link { .ci-status-link {
display: inline-block; display: inline-flex;
position: relative;
top: 2px;
} }
.btn-clipboard, > .ci-status-link,
.btn-transparent { > .btn,
padding-left: 0; > .commit-sha-group {
padding-right: 0; margin-left: $gl-padding-8;
} }
}
.commit-sha-group {
display: inline-flex;
.label,
.btn { .btn {
&:not(:first-child) { padding: $gl-vert-padding $gl-btn-padding;
margin-left: $gl-padding; border: 1px $border-color solid;
} font-size: $gl-font-size;
line-height: $line-height-base;
border-radius: 0;
display: flex;
align-items: center;
}
.label-monospace {
@extend .monospace;
user-select: text;
color: $gl-text-color;
background-color: $gray-light;
} }
.commit-sha { .btn svg {
font-size: 14px; top: auto;
font-weight: $gl-font-weight-bold; fill: $gl-text-color-secondary;
} }
.ci-status-icon { .fa-clipboard {
position: relative; color: $gl-text-color-secondary;
top: 2px; }
:first-child {
border-bottom-left-radius: $border-radius-default;
border-top-left-radius: $border-radius-default;
}
:not(:first-child) {
border-left: 0;
}
:last-child {
border-bottom-right-radius: $border-radius-default;
border-top-right-radius: $border-radius-default;
} }
} }
.commit, .commit,
.generic_commit_status { .generic_commit_status {
a, a,
button { button {
color: $gl-text-color; color: $gl-text-color;
...@@ -307,10 +332,8 @@ ...@@ -307,10 +332,8 @@
} }
} }
.gpg-status-box { .gpg-status-box {
padding: 2px 10px; padding: 2px 10px;
margin-right: $gl-padding;
&:empty { &:empty {
display: none; display: none;
......
...@@ -10,7 +10,7 @@ module AuthenticatesWithTwoFactor ...@@ -10,7 +10,7 @@ module AuthenticatesWithTwoFactor
# This action comes from DeviseController, but because we call `sign_in` # This action comes from DeviseController, but because we call `sign_in`
# manually, not skipping this action would cause a "You are already signed # manually, not skipping this action would cause a "You are already signed
# in." error message to be shown upon successful login. # in." error message to be shown upon successful login.
skip_before_action :require_no_authentication, only: [:create] skip_before_action :require_no_authentication, only: [:create], raise: false
end end
# Store the user's ID in the session for later retrieval and render the # Store the user's ID in the session for later retrieval and render the
......
class Projects::RepositoriesController < Projects::ApplicationController class Projects::RepositoriesController < Projects::ApplicationController
include ExtractsPath
# Authorize # Authorize
before_action :require_non_empty_project, except: :create before_action :require_non_empty_project, except: :create
before_action :assign_archive_vars, only: :archive
before_action :authorize_download_code! before_action :authorize_download_code!
before_action :authorize_admin_project!, only: :create before_action :authorize_admin_project!, only: :create
...@@ -11,9 +14,21 @@ class Projects::RepositoriesController < Projects::ApplicationController ...@@ -11,9 +14,21 @@ class Projects::RepositoriesController < Projects::ApplicationController
end end
def archive def archive
send_git_archive @repository, ref: params[:ref], format: params[:format] append_sha = params[:append_sha]
shortname = "#{@project.path}-#{@ref.tr('/', '-')}"
append_sha = false if @filename == shortname
send_git_archive @repository, ref: @ref, format: params[:format], append_sha: append_sha
rescue => ex rescue => ex
logger.error("#{self.class.name}: #{ex}") logger.error("#{self.class.name}: #{ex}")
return git_not_found! return git_not_found!
end end
def assign_archive_vars
@id = params[:id]
@ref, @filename = extract_ref(@id)
rescue InvalidPathError
render_404
end
end end
...@@ -93,25 +93,18 @@ module CommitsHelper ...@@ -93,25 +93,18 @@ module CommitsHelper
return unless current_controller?(:commits) return unless current_controller?(:commits)
if @path.blank? if @path.blank?
return link_to( url = project_tree_path(project, commit)
_("Browse Files"), tooltip = _("Browse Files")
project_tree_path(project, commit),
class: "btn btn-default"
)
elsif @repo.blob_at(commit.id, @path) elsif @repo.blob_at(commit.id, @path)
return link_to( url = project_blob_path(project, tree_join(commit.id, @path))
_("Browse File"), tooltip = _("Browse File")
project_blob_path(project,
tree_join(commit.id, @path)),
class: "btn btn-default"
)
elsif @path.present? elsif @path.present?
return link_to( url = project_tree_path(project, tree_join(commit.id, @path))
_("Browse Directory"), tooltip = _("Browse Directory")
project_tree_path(project, end
tree_join(commit.id, @path)),
class: "btn btn-default" link_to url, class: "btn btn-default has-tooltip", title: tooltip, data: { container: "body" } do
) sprite_icon('folder-open')
end end
end end
......
...@@ -24,8 +24,8 @@ module WorkhorseHelper ...@@ -24,8 +24,8 @@ module WorkhorseHelper
end end
# Archive a Git repository and send it through Workhorse # Archive a Git repository and send it through Workhorse
def send_git_archive(repository, ref:, format:) def send_git_archive(repository, **kwargs)
headers.store(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format)) headers.store(*Gitlab::Workhorse.send_git_archive(repository, **kwargs))
head :ok head :ok
end end
......
...@@ -330,6 +330,10 @@ class Group < Namespace ...@@ -330,6 +330,10 @@ class Group < Namespace
false false
end end
def refresh_project_authorizations
refresh_members_authorized_projects(blocking: false)
end
private private
def update_two_factor_requirement def update_two_factor_requirement
......
...@@ -262,6 +262,10 @@ class Namespace < ActiveRecord::Base ...@@ -262,6 +262,10 @@ class Namespace < ActiveRecord::Base
[] []
end end
def refresh_project_authorizations
owner.refresh_authorized_projects
end
private private
def path_or_parent_changed? def path_or_parent_changed?
......
...@@ -1486,7 +1486,9 @@ class Project < ActiveRecord::Base ...@@ -1486,7 +1486,9 @@ class Project < ActiveRecord::Base
end end
def rename_repo_notify! def rename_repo_notify!
send_move_instructions(full_path_was) # When we import a project overwriting the original project, there
# is a move operation. In that case we don't want to send the instructions.
send_move_instructions(full_path_was) unless started?
expires_full_path_cache expires_full_path_cache
self.old_path_with_namespace = full_path_was self.old_path_with_namespace = full_path_was
......
module Projects
class BaseMoveRelationsService < BaseService
attr_reader :source_project
def execute(source_project, remove_remaining_elements: true)
return if source_project.blank?
@source_project = source_project
true
end
private
def prepare_relation(relation, id_param = :id)
if Gitlab::Database.postgresql?
relation
else
relation.model.where("#{id_param}": relation.pluck(id_param))
end
end
end
end
...@@ -47,6 +47,20 @@ module Projects ...@@ -47,6 +47,20 @@ module Projects
raise raise
end end
def attempt_repositories_rollback
return unless @project
flush_caches(@project)
unless rollback_repository(removal_path(repo_path), repo_path)
raise_error('Failed to restore project repository. Please contact the administrator.')
end
unless rollback_repository(removal_path(wiki_path), wiki_path)
raise_error('Failed to restore wiki repository. Please contact the administrator.')
end
end
private private
def repo_path def repo_path
...@@ -72,11 +86,11 @@ module Projects ...@@ -72,11 +86,11 @@ module Projects
return true if params[:skip_repo] == true return true if params[:skip_repo] == true
# There is a possibility project does not have repository or wiki # There is a possibility project does not have repository or wiki
return true unless gitlab_shell.exists?(project.repository_storage_path, path + '.git') return true unless repo_exists?(path)
new_path = removal_path(path) new_path = removal_path(path)
if gitlab_shell.mv_repository(project.repository_storage_path, path, new_path) if mv_repository(path, new_path)
log_info("Repository \"#{path}\" moved to \"#{new_path}\"") log_info("Repository \"#{path}\" moved to \"#{new_path}\"")
project.run_after_commit do project.run_after_commit do
...@@ -88,6 +102,21 @@ module Projects ...@@ -88,6 +102,21 @@ module Projects
end end
end end
def rollback_repository(old_path, new_path)
# There is a possibility project does not have repository or wiki
return true unless repo_exists?(old_path)
mv_repository(old_path, new_path)
end
def repo_exists?(path)
gitlab_shell.exists?(project.repository_storage_path, path + '.git')
end
def mv_repository(from_path, to_path)
gitlab_shell.mv_repository(project.repository_storage_path, from_path, to_path)
end
def attempt_rollback(project, message) def attempt_rollback(project, message)
return unless project return unless project
......
...@@ -15,9 +15,18 @@ module Projects ...@@ -15,9 +15,18 @@ module Projects
file = params.delete(:file) file = params.delete(:file)
FileUtils.copy_entry(file.path, import_upload_path) FileUtils.copy_entry(file.path, import_upload_path)
@overwrite = params.delete(:overwrite)
data = {}
data[:override_params] = @override_params if @override_params
if overwrite_project?
data[:original_path] = params[:path]
params[:path] += "-#{tmp_filename}"
end
params[:import_type] = 'gitlab_project' params[:import_type] = 'gitlab_project'
params[:import_source] = import_upload_path params[:import_source] = import_upload_path
params[:import_data] = { data: { override_params: @override_params } } if @override_params params[:import_data] = { data: data } if data.present?
::Projects::CreateService.new(current_user, params).execute ::Projects::CreateService.new(current_user, params).execute
end end
...@@ -31,5 +40,17 @@ module Projects ...@@ -31,5 +40,17 @@ module Projects
def tmp_filename def tmp_filename
SecureRandom.hex SecureRandom.hex
end end
def overwrite_project?
@overwrite && project_with_same_full_path?
end
def project_with_same_full_path?
Project.find_by_full_path("#{current_namespace.full_path}/#{params[:path]}").present?
end
def current_namespace
@current_namespace ||= Namespace.find_by(id: params[:namespace_id])
end
end end
end end
module Projects
class MoveAccessService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
return unless super
@project.with_transaction_returning_status do
if @project.namespace != source_project.namespace
@project.run_after_commit do
source_project.namespace.refresh_project_authorizations
self.namespace.refresh_project_authorizations
end
end
::Projects::MoveProjectMembersService.new(@project, @current_user)
.execute(source_project, remove_remaining_elements: remove_remaining_elements)
::Projects::MoveProjectGroupLinksService.new(@project, @current_user)
.execute(source_project, remove_remaining_elements: remove_remaining_elements)
::Projects::MoveProjectAuthorizationsService.new(@project, @current_user)
.execute(source_project, remove_remaining_elements: remove_remaining_elements)
success
end
end
end
end
module Projects
class MoveDeployKeysProjectsService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
return unless super
Project.transaction(requires_new: true) do
move_deploy_keys_projects
remove_remaining_deploy_keys_projects if remove_remaining_elements
success
end
end
private
def move_deploy_keys_projects
prepare_relation(non_existent_deploy_keys_projects)
.update_all(project_id: @project.id)
end
def non_existent_deploy_keys_projects
source_project.deploy_keys_projects
.joins(:deploy_key)
.where.not(keys: { fingerprint: @project.deploy_keys.select(:fingerprint) })
end
def remove_remaining_deploy_keys_projects
source_project.deploy_keys_projects.destroy_all
end
end
end
module Projects
class MoveForksService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
return unless super && source_project.fork_network
Project.transaction(requires_new: true) do
move_forked_project_links
move_fork_network_members
update_root_project
refresh_forks_count
success
end
end
private
def move_forked_project_links
# Update ancestor
ForkedProjectLink.where(forked_to_project: source_project)
.update_all(forked_to_project_id: @project.id)
# Update the descendants
ForkedProjectLink.where(forked_from_project: source_project)
.update_all(forked_from_project_id: @project.id)
end
def move_fork_network_members
ForkNetworkMember.where(project: source_project).update_all(project_id: @project.id)
ForkNetworkMember.where(forked_from_project: source_project).update_all(forked_from_project_id: @project.id)
end
def update_root_project
# Update root network project
ForkNetwork.where(root_project: source_project).update_all(root_project_id: @project.id)
end
def refresh_forks_count
Projects::ForksCountService.new(@project).refresh_cache
end
end
end
module Projects
class MoveLfsObjectsProjectsService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
return unless super
Project.transaction(requires_new: true) do
move_lfs_objects_projects
remove_remaining_lfs_objects_project if remove_remaining_elements
success
end
end
private
def move_lfs_objects_projects
prepare_relation(non_existent_lfs_objects_projects)
.update_all(project_id: @project.lfs_storage_project.id)
end
def remove_remaining_lfs_objects_project
source_project.lfs_objects_projects.destroy_all
end
def non_existent_lfs_objects_projects
source_project.lfs_objects_projects.where.not(lfs_object: @project.lfs_objects)
end
end
end
module Projects
class MoveNotificationSettingsService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
return unless super
Project.transaction(requires_new: true) do
move_notification_settings
remove_remaining_notification_settings if remove_remaining_elements
success
end
end
private
def move_notification_settings
prepare_relation(non_existent_notifications)
.update_all(source_id: @project.id)
end
# Remove remaining notification settings from source_project
def remove_remaining_notification_settings
source_project.notification_settings.destroy_all
end
# Get users of current notification_settings
def users_in_target_project
@project.notification_settings.select(:user_id)
end
# Look for notification_settings in source_project that are not in the target project
def non_existent_notifications
source_project.notification_settings
.select(:id)
.where.not(user_id: users_in_target_project)
end
end
end
# NOTE: This service cannot be used directly because it is part of a
# a bigger process. Instead, use the service MoveAccessService which moves
# project memberships, project group links, authorizations and refreshes
# the authorizations if neccessary
module Projects
class MoveProjectAuthorizationsService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
return unless super
Project.transaction(requires_new: true) do
move_project_authorizations
remove_remaining_authorizations if remove_remaining_elements
success
end
end
private
def move_project_authorizations
prepare_relation(non_existent_authorization, :user_id)
.update_all(project_id: @project.id)
end
def remove_remaining_authorizations
# I think because the Project Authorization table does not have a primary key
# it brings a lot a problems/bugs. First, Rails raises PG::SyntaxException if we use
# destroy_all instead of delete_all.
source_project.project_authorizations.delete_all(:delete_all)
end
# Look for authorizations in source_project that are not in the target project
def non_existent_authorization
source_project.project_authorizations
.select(:user_id)
.where.not(user: @project.authorized_users)
end
end
end
# NOTE: This service cannot be used directly because it is part of a
# a bigger process. Instead, use the service MoveAccessService which moves
# project memberships, project group links, authorizations and refreshes
# the authorizations if neccessary
module Projects
class MoveProjectGroupLinksService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
return unless super
Project.transaction(requires_new: true) do
move_group_links
remove_remaining_project_group_links if remove_remaining_elements
success
end
end
private
def move_group_links
prepare_relation(non_existent_group_links)
.update_all(project_id: @project.id)
end
# Remove remaining project group links from source_project
def remove_remaining_project_group_links
source_project.reload.project_group_links.destroy_all
end
def group_links_in_target_project
@project.project_group_links.select(:group_id)
end
# Look for groups in source_project that are not in the target project
def non_existent_group_links
source_project.project_group_links
.where.not(group_id: group_links_in_target_project)
end
end
end
# NOTE: This service cannot be used directly because it is part of a
# a bigger process. Instead, use the service MoveAccessService which moves
# project memberships, project group links, authorizations and refreshes
# the authorizations if neccessary
module Projects
class MoveProjectMembersService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
return unless super
Project.transaction(requires_new: true) do
move_project_members
remove_remaining_members if remove_remaining_elements
success
end
end
private
def move_project_members
prepare_relation(non_existent_members).update_all(source_id: @project.id)
end
def remove_remaining_members
# Remove remaining members and authorizations from source_project
source_project.project_members.destroy_all
end
def project_members_in_target_project
@project.project_members.select(:user_id)
end
# Look for members in source_project that are not in the target project
def non_existent_members
source_project.members
.select(:id)
.where.not(user_id: @project.project_members.select(:user_id))
end
end
end
module Projects
class MoveUsersStarProjectsService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
return unless super
user_stars = source_project.users_star_projects
return unless user_stars.any?
Project.transaction(requires_new: true) do
user_stars.update_all(project_id: @project.id)
Project.reset_counters @project.id, :users_star_projects
Project.reset_counters source_project.id, :users_star_projects
success
end
end
end
end
module Projects
class OverwriteProjectService < BaseService
def execute(source_project)
return unless source_project && source_project.namespace == @project.namespace
Project.transaction do
move_before_destroy_relationships(source_project)
destroy_old_project(source_project)
rename_project(source_project.name, source_project.path)
@project
end
# Projects::DestroyService can raise Exceptions, but we don't want
# to pass that kind of exception to the caller. Instead, we change it
# for a StandardError exception
rescue Exception => e # rubocop:disable Lint/RescueException
attempt_restore_repositories(source_project)
if e.class == Exception
raise StandardError, e.message
else
raise
end
end
private
def move_before_destroy_relationships(source_project)
options = { remove_remaining_elements: false }
::Projects::MoveUsersStarProjectsService.new(@project, @current_user).execute(source_project, options)
::Projects::MoveAccessService.new(@project, @current_user).execute(source_project, options)
::Projects::MoveDeployKeysProjectsService.new(@project, @current_user).execute(source_project, options)
::Projects::MoveNotificationSettingsService.new(@project, @current_user).execute(source_project, options)
::Projects::MoveForksService.new(@project, @current_user).execute(source_project, options)
::Projects::MoveLfsObjectsProjectsService.new(@project, @current_user).execute(source_project, options)
add_source_project_to_fork_network(source_project)
end
def destroy_old_project(source_project)
# Delete previous project (synchronously) and unlink relations
::Projects::DestroyService.new(source_project, @current_user).execute
end
def rename_project(name, path)
# Update de project's name and path to the original name/path
::Projects::UpdateService.new(@project,
@current_user,
{ name: name, path: path })
.execute
end
def attempt_restore_repositories(project)
::Projects::DestroyService.new(project, @current_user).attempt_repositories_rollback
end
def add_source_project_to_fork_network(source_project)
return unless @project.fork_network
# Because he have moved all references in the fork network from the source_project
# we won't be able to query the database (only through its cached data),
# for its former relationships. That's why we're adding it to the network
# as a fork of the target project
ForkNetworkMember.create!(fork_network: @project.fork_network,
project: source_project,
forked_from_project: @project)
end
end
end
- pipeline = local_assigns.fetch(:pipeline) { project.latest_successful_pipeline_for(ref) } - pipeline = local_assigns.fetch(:pipeline) { project.latest_successful_pipeline_for(ref) }
- if !project.empty_repo? && can?(current_user, :download_code, project) - if !project.empty_repo? && can?(current_user, :download_code, project)
- archive_prefix = "#{project.path}-#{ref.tr('/', '-')}"
.project-action-button.dropdown.inline> .project-action-button.dropdown.inline>
%button.btn.has-tooltip{ title: s_('DownloadSource|Download'), 'data-toggle' => 'dropdown', 'aria-label' => s_('DownloadSource|Download') } %button.btn.has-tooltip{ title: s_('DownloadSource|Download'), 'data-toggle' => 'dropdown', 'aria-label' => s_('DownloadSource|Download') }
= sprite_icon('download') = sprite_icon('download')
...@@ -10,16 +11,16 @@ ...@@ -10,16 +11,16 @@
%li.dropdown-header %li.dropdown-header
#{ _('Source code') } #{ _('Source code') }
%li %li
= link_to archive_project_repository_path(project, ref: ref, format: 'zip'), rel: 'nofollow', download: '' do = link_to project_archive_path(project, id: tree_join(ref, archive_prefix), format: 'zip'), rel: 'nofollow', download: '' do
%span= _('Download zip') %span= _('Download zip')
%li %li
= link_to archive_project_repository_path(project, ref: ref, format: 'tar.gz'), rel: 'nofollow', download: '' do = link_to project_archive_path(project, id: tree_join(ref, archive_prefix), format: 'tar.gz'), rel: 'nofollow', download: '' do
%span= _('Download tar.gz') %span= _('Download tar.gz')
%li %li
= link_to archive_project_repository_path(project, ref: ref, format: 'tar.bz2'), rel: 'nofollow', download: '' do = link_to project_archive_path(project, id: tree_join(ref, archive_prefix), format: 'tar.bz2'), rel: 'nofollow', download: '' do
%span= _('Download tar.bz2') %span= _('Download tar.bz2')
%li %li
= link_to archive_project_repository_path(project, ref: ref, format: 'tar'), rel: 'nofollow', download: '' do = link_to project_archive_path(project, id: tree_join(ref, archive_prefix), format: 'tar'), rel: 'nofollow', download: '' do
%span= _('Download tar') %span= _('Download tar')
- if pipeline && pipeline.latest_builds_with_artifacts.any? - if pipeline && pipeline.latest_builds_with_artifacts.any?
......
...@@ -26,7 +26,10 @@ ...@@ -26,7 +26,10 @@
.commit-detail.flex-list .commit-detail.flex-list
.commit-content .commit-content
= link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title") - if view_details && merge_request
= link_to commit.title, project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: "commit-row-message item-title"
- else
= link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title")
%span.commit-row-message.visible-xs-inline %span.commit-row-message.visible-xs-inline
&middot; &middot;
= commit.short_id = commit.short_id
...@@ -59,9 +62,9 @@ ...@@ -59,9 +62,9 @@
= render_commit_status(commit, ref: ref) = render_commit_status(commit, ref: ref)
.js-commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id) } } .js-commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id) } }
= link_to commit.short_id, link, class: "commit-sha btn btn-transparent btn-link"
= clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard"))
= link_to_browse_code(project, commit)
- if view_details && merge_request .commit-sha-group
= link_to "View details", project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: "btn btn-default" .label.label-monospace
= commit.short_id
= clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard"), class: "btn btn-default", container: "body")
= link_to_browse_code(project, commit)
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
= hidden_field(resource_name, field, value: value) = hidden_field(resource_name, field, value: value)
= hidden_field_tag(:spam_log_id, spammable.spam_log.id) = hidden_field_tag(:spam_log_id, spammable.spam_log.id)
= hidden_field_tag(:recaptcha_verification, true) = hidden_field_tag(:recaptcha_verification, true)
= recaptcha_tags script: script, callback: 'recaptchaDialogCallback' = recaptcha_tags script: script, callback: 'recaptchaDialogCallback' unless Rails.env.test?
-# Yields a block with given extra params. -# Yields a block with given extra params.
= yield = yield
......
---
title: Improved visual styles and consistency for commit hash and possible actions
across commit lists
merge_request: 17406
author:
type: changed
---
title: Extend API for importing a project export with overwrite support
merge_request: 17883
author:
type: added
---
title: Add alternate archive route for simplified packaging
merge_request: 17225
author:
type: added
---
title: Remove support for legacy tar.gz pages artifacts
merge_request: 18090
author:
type: deprecated
...@@ -287,6 +287,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -287,6 +287,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end end
scope '-' do scope '-' do
get 'archive/*id', constraints: { format: Gitlab::PathRegex.archive_formats_regex, id: /.+?/ }, to: 'repositories#archive', as: 'archive'
resources :jobs, only: [:index, :show], constraints: { id: /\d+/ } do resources :jobs, only: [:index, :show], constraints: { id: /\d+/ } do
collection do collection do
post :cancel_all post :cancel_all
......
...@@ -2,10 +2,11 @@ ...@@ -2,10 +2,11 @@
resource :repository, only: [:create] do resource :repository, only: [:create] do
member do member do
get ':ref/archive', constraints: { format: Gitlab::PathRegex.archive_formats_regex, ref: /.+/ }, action: 'archive', as: 'archive'
# deprecated since GitLab 9.5 # deprecated since GitLab 9.5
get 'archive', constraints: { format: Gitlab::PathRegex.archive_formats_regex }, as: 'archive_alternative' get 'archive', constraints: { format: Gitlab::PathRegex.archive_formats_regex }, as: 'archive_alternative', defaults: { append_sha: true }
# deprecated since GitLab 10.7
get ':id/archive', constraints: { format: Gitlab::PathRegex.archive_formats_regex, id: /.+/ }, action: 'archive', as: 'archive_deprecated', defaults: { append_sha: true }
end end
end end
......
...@@ -111,6 +111,7 @@ POST /projects/import ...@@ -111,6 +111,7 @@ POST /projects/import
| `namespace` | integer/string | no | The ID or path of the namespace that the project will be imported to. Defaults to the current user's namespace | | `namespace` | integer/string | no | The ID or path of the namespace that the project will be imported to. Defaults to the current user's namespace |
| `file` | string | yes | The file to be uploaded | | `file` | string | yes | The file to be uploaded |
| `path` | string | yes | Name and path for new project | | `path` | string | yes | Name and path for new project |
| `overwrite` | boolean | no | If there is a project with the same path the import will overwrite it. Default to false |
| `override_params` | Hash | no | Supports all fields defined in the [Project API](projects.md)] | | `override_params` | Hash | no | Supports all fields defined in the [Project API](projects.md)] |
The override params passed will take precendence over all values defined inside the export file. The override params passed will take precendence over all values defined inside the export file.
......
...@@ -507,8 +507,8 @@ module API ...@@ -507,8 +507,8 @@ module API
header(*Gitlab::Workhorse.send_git_blob(repository, blob)) header(*Gitlab::Workhorse.send_git_blob(repository, blob))
end end
def send_git_archive(repository, ref:, format:) def send_git_archive(repository, **kwargs)
header(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format)) header(*Gitlab::Workhorse.send_git_archive(repository, **kwargs))
end end
def send_artifacts_entry(build, entry) def send_artifacts_entry(build, entry)
......
...@@ -26,6 +26,7 @@ module API ...@@ -26,6 +26,7 @@ module API
requires :path, type: String, desc: 'The new project path and name' requires :path, type: String, desc: 'The new project path and name'
requires :file, type: File, desc: 'The project export file to be imported' requires :file, type: File, desc: 'The project export file to be imported'
optional :namespace, type: String, desc: "The ID or name of the namespace that the project will be imported into. Defaults to the current user's namespace." optional :namespace, type: String, desc: "The ID or name of the namespace that the project will be imported into. Defaults to the current user's namespace."
optional :overwrite, type: Boolean, default: false, desc: 'If there is a project in the same namespace and with the same name overwrite it'
optional :override_params, optional :override_params,
type: Hash, type: Hash,
desc: 'New project params to override values in the export' do desc: 'New project params to override values in the export' do
...@@ -50,7 +51,8 @@ module API ...@@ -50,7 +51,8 @@ module API
project_params = { project_params = {
path: import_params[:path], path: import_params[:path],
namespace_id: namespace.id, namespace_id: namespace.id,
file: import_params[:file]['tempfile'] file: import_params[:file]['tempfile'],
overwrite: import_params[:overwrite]
} }
override_params = import_params.delete(:override_params) override_params = import_params.delete(:override_params)
......
...@@ -88,7 +88,7 @@ module API ...@@ -88,7 +88,7 @@ module API
end end
get ':id/repository/archive', requirements: { format: Gitlab::PathRegex.archive_formats_regex } do get ':id/repository/archive', requirements: { format: Gitlab::PathRegex.archive_formats_regex } do
begin begin
send_git_archive user_project.repository, ref: params[:sha], format: params[:format] send_git_archive user_project.repository, ref: params[:sha], format: params[:format], append_sha: true
rescue rescue
not_found!('File') not_found!('File')
end end
......
...@@ -75,7 +75,7 @@ module API ...@@ -75,7 +75,7 @@ module API
end end
get ':id/repository/archive', requirements: { format: Gitlab::PathRegex.archive_formats_regex } do get ':id/repository/archive', requirements: { format: Gitlab::PathRegex.archive_formats_regex } do
begin begin
send_git_archive user_project.repository, ref: params[:sha], format: params[:format] send_git_archive user_project.repository, ref: params[:sha], format: params[:format], append_sha: true
rescue rescue
not_found!('File') not_found!('File')
end end
......
...@@ -396,17 +396,24 @@ module Gitlab ...@@ -396,17 +396,24 @@ module Gitlab
nil nil
end end
def archive_prefix(ref, sha) def archive_prefix(ref, sha, append_sha:)
append_sha = (ref != sha) if append_sha.nil?
project_name = self.name.chomp('.git') project_name = self.name.chomp('.git')
"#{project_name}-#{ref.tr('/', '-')}-#{sha}" formatted_ref = ref.tr('/', '-')
prefix_segments = [project_name, formatted_ref]
prefix_segments << sha if append_sha
prefix_segments.join('-')
end end
def archive_metadata(ref, storage_path, format = "tar.gz") def archive_metadata(ref, storage_path, format = "tar.gz", append_sha:)
ref ||= root_ref ref ||= root_ref
commit = Gitlab::Git::Commit.find(self, ref) commit = Gitlab::Git::Commit.find(self, ref)
return {} if commit.nil? return {} if commit.nil?
prefix = archive_prefix(ref, commit.id) prefix = archive_prefix(ref, commit.id, append_sha: append_sha)
{ {
'RepoPath' => path, 'RepoPath' => path,
......
module Gitlab module Gitlab
module ImportExport module ImportExport
class Importer class Importer
include Gitlab::Allowable
include Gitlab::Utils::StrongMemoize
def self.imports_repository? def self.imports_repository?
true true
end end
...@@ -13,12 +16,14 @@ module Gitlab ...@@ -13,12 +16,14 @@ module Gitlab
end end
def execute def execute
if import_file && check_version! && restorers.all?(&:restore) if import_file && check_version! && restorers.all?(&:restore) && overwrite_project
project_tree.restored_project project_tree.restored_project
else else
raise Projects::ImportService::Error.new(@shared.errors.join(', ')) raise Projects::ImportService::Error.new(@shared.errors.join(', '))
end end
rescue => e
raise Projects::ImportService::Error.new(e.message)
ensure
remove_import_file remove_import_file
end end
...@@ -26,7 +31,7 @@ module Gitlab ...@@ -26,7 +31,7 @@ module Gitlab
def restorers def restorers
[repo_restorer, wiki_restorer, project_tree, avatar_restorer, [repo_restorer, wiki_restorer, project_tree, avatar_restorer,
uploads_restorer, lfs_restorer] uploads_restorer, lfs_restorer, statistics_restorer]
end end
def import_file def import_file
...@@ -69,6 +74,10 @@ module Gitlab ...@@ -69,6 +74,10 @@ module Gitlab
Gitlab::ImportExport::LfsRestorer.new(project: project_tree.restored_project, shared: @shared) Gitlab::ImportExport::LfsRestorer.new(project: project_tree.restored_project, shared: @shared)
end end
def statistics_restorer
Gitlab::ImportExport::StatisticsRestorer.new(project: project_tree.restored_project, shared: @shared)
end
def path_with_namespace def path_with_namespace
File.join(@project.namespace.full_path, @project.path) File.join(@project.namespace.full_path, @project.path)
end end
...@@ -84,6 +93,33 @@ module Gitlab ...@@ -84,6 +93,33 @@ module Gitlab
def remove_import_file def remove_import_file
FileUtils.rm_rf(@archive_file) FileUtils.rm_rf(@archive_file)
end end
def overwrite_project
project = project_tree.restored_project
return unless can?(@current_user, :admin_namespace, project.namespace)
if overwrite_project?
::Projects::OverwriteProjectService.new(project, @current_user)
.execute(project_to_overwrite)
end
true
end
def original_path
@project.import_data&.data&.fetch('original_path', nil)
end
def overwrite_project?
original_path.present? && project_to_overwrite.present?
end
def project_to_overwrite
strong_memoize(:project_to_overwrite) do
Project.find_by_full_path("#{@project.namespace.full_path}/#{original_path}")
end
end
end end
end end
end end
...@@ -92,7 +92,7 @@ module Gitlab ...@@ -92,7 +92,7 @@ module Gitlab
end end
def override_params def override_params
return {} unless params = @project.import_data&.data&.fetch('override_params') return {} unless params = @project.import_data&.data&.fetch('override_params', nil)
@override_params ||= params.select do |key, _value| @override_params ||= params.select do |key, _value|
Project.column_names.include?(key.to_s) && Project.column_names.include?(key.to_s) &&
......
module Gitlab
module ImportExport
class StatisticsRestorer
def initialize(project:, shared:)
@project = project
@shared = shared
end
def restore
@project.statistics.refresh!
rescue => e
@shared.error(e)
false
end
end
end
end
...@@ -59,10 +59,10 @@ module Gitlab ...@@ -59,10 +59,10 @@ module Gitlab
] ]
end end
def send_git_archive(repository, ref:, format:) def send_git_archive(repository, ref:, format:, append_sha:)
format ||= 'tar.gz' format ||= 'tar.gz'
format.downcase! format.downcase!
params = repository.archive_metadata(ref, Gitlab.config.gitlab.repository_downloads_path, format) params = repository.archive_metadata(ref, Gitlab.config.gitlab.repository_downloads_path, format, append_sha: append_sha)
raise "Repository or ref not found" if params.empty? raise "Repository or ref not found" if params.empty?
if Gitlab::GitalyClient.feature_enabled?(:workhorse_archive, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) if Gitlab::GitalyClient.feature_enabled?(:workhorse_archive, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT)
......
...@@ -6,7 +6,7 @@ describe Projects::RepositoriesController do ...@@ -6,7 +6,7 @@ describe Projects::RepositoriesController do
describe "GET archive" do describe "GET archive" do
context 'as a guest' do context 'as a guest' do
it 'responds with redirect in correct format' do it 'responds with redirect in correct format' do
get :archive, namespace_id: project.namespace, project_id: project, format: "zip", ref: 'master' get :archive, namespace_id: project.namespace, project_id: project, id: "master", format: "zip"
expect(response.header["Content-Type"]).to start_with('text/html') expect(response.header["Content-Type"]).to start_with('text/html')
expect(response).to be_redirect expect(response).to be_redirect
...@@ -22,18 +22,25 @@ describe Projects::RepositoriesController do ...@@ -22,18 +22,25 @@ describe Projects::RepositoriesController do
end end
it "uses Gitlab::Workhorse" do it "uses Gitlab::Workhorse" do
get :archive, namespace_id: project.namespace, project_id: project, ref: "master", format: "zip" get :archive, namespace_id: project.namespace, project_id: project, id: "master", format: "zip"
expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:") expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:")
end end
it 'responds with redirect to the short name archive if fully qualified' do
get :archive, namespace_id: project.namespace, project_id: project, id: "master/#{project.path}-master", format: "zip"
expect(assigns(:ref)).to eq("master")
expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:")
end
context "when the service raises an error" do context "when the service raises an error" do
before do before do
allow(Gitlab::Workhorse).to receive(:send_git_archive).and_raise("Archive failed") allow(Gitlab::Workhorse).to receive(:send_git_archive).and_raise("Archive failed")
end end
it "renders Not Found" do it "renders Not Found" do
get :archive, namespace_id: project.namespace, project_id: project, ref: "master", format: "zip" get :archive, namespace_id: project.namespace, project_id: project, id: "master", format: "zip"
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(404)
end end
......
FactoryBot.define do
factory :users_star_project do
project
user
end
end
...@@ -34,9 +34,6 @@ describe 'New issue', :js do ...@@ -34,9 +34,6 @@ describe 'New issue', :js do
click_button 'Submit issue' click_button 'Submit issue'
# reCAPTCHA alerts when it can't contact the server, so just accept it and move on
page.driver.browser.switch_to.alert.accept
# it is impossible to test recaptcha automatically and there is no possibility to fill in recaptcha # it is impossible to test recaptcha automatically and there is no possibility to fill in recaptcha
# recaptcha verification is skipped in test environment and it always returns true # recaptcha verification is skipped in test environment and it always returns true
expect(page).not_to have_content('issue title') expect(page).not_to have_content('issue title')
......
...@@ -247,38 +247,44 @@ describe Gitlab::Git::Repository, seed_helper: true do ...@@ -247,38 +247,44 @@ describe Gitlab::Git::Repository, seed_helper: true do
end end
it 'returns parameterised string for a ref containing slashes' do it 'returns parameterised string for a ref containing slashes' do
prefix = repository.archive_prefix('test/branch', 'SHA') prefix = repository.archive_prefix('test/branch', 'SHA', append_sha: nil)
expect(prefix).to eq("#{project_name}-test-branch-SHA") expect(prefix).to eq("#{project_name}-test-branch-SHA")
end end
it 'returns correct string for a ref containing dots' do it 'returns correct string for a ref containing dots' do
prefix = repository.archive_prefix('test.branch', 'SHA') prefix = repository.archive_prefix('test.branch', 'SHA', append_sha: nil)
expect(prefix).to eq("#{project_name}-test.branch-SHA") expect(prefix).to eq("#{project_name}-test.branch-SHA")
end end
it 'returns string with sha when append_sha is false' do
prefix = repository.archive_prefix('test.branch', 'SHA', append_sha: false)
expect(prefix).to eq("#{project_name}-test.branch")
end
end end
describe '#archive' do describe '#archive' do
let(:metadata) { repository.archive_metadata('master', '/tmp') } let(:metadata) { repository.archive_metadata('master', '/tmp', append_sha: true) }
it_should_behave_like 'archive check', '.tar.gz' it_should_behave_like 'archive check', '.tar.gz'
end end
describe '#archive_zip' do describe '#archive_zip' do
let(:metadata) { repository.archive_metadata('master', '/tmp', 'zip') } let(:metadata) { repository.archive_metadata('master', '/tmp', 'zip', append_sha: true) }
it_should_behave_like 'archive check', '.zip' it_should_behave_like 'archive check', '.zip'
end end
describe '#archive_bz2' do describe '#archive_bz2' do
let(:metadata) { repository.archive_metadata('master', '/tmp', 'tbz2') } let(:metadata) { repository.archive_metadata('master', '/tmp', 'tbz2', append_sha: true) }
it_should_behave_like 'archive check', '.tar.bz2' it_should_behave_like 'archive check', '.tar.bz2'
end end
describe '#archive_fallback' do describe '#archive_fallback' do
let(:metadata) { repository.archive_metadata('master', '/tmp', 'madeup') } let(:metadata) { repository.archive_metadata('master', '/tmp', 'madeup', append_sha: true) }
it_should_behave_like 'archive check', '.tar.gz' it_should_behave_like 'archive check', '.tar.gz'
end end
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::ImportExport::Importer do describe Gitlab::ImportExport::Importer do
let(:user) { create(:user) }
let(:test_path) { "#{Dir.tmpdir}/importer_spec" } let(:test_path) { "#{Dir.tmpdir}/importer_spec" }
let(:shared) { project.import_export_shared } let(:shared) { project.import_export_shared }
let(:project) { create(:project, import_source: File.join(test_path, 'exported-project.gz')) } let(:project) { create(:project, import_source: File.join(test_path, 'exported-project.gz')) }
...@@ -11,6 +12,7 @@ describe Gitlab::ImportExport::Importer do ...@@ -11,6 +12,7 @@ describe Gitlab::ImportExport::Importer do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(test_path) allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(test_path)
FileUtils.mkdir_p(shared.export_path) FileUtils.mkdir_p(shared.export_path)
FileUtils.cp(Rails.root.join('spec', 'fixtures', 'exported-project.gz'), test_path) FileUtils.cp(Rails.root.join('spec', 'fixtures', 'exported-project.gz'), test_path)
allow(subject).to receive(:remove_import_file)
end end
after do after do
...@@ -42,7 +44,8 @@ describe Gitlab::ImportExport::Importer do ...@@ -42,7 +44,8 @@ describe Gitlab::ImportExport::Importer do
Gitlab::ImportExport::RepoRestorer, Gitlab::ImportExport::RepoRestorer,
Gitlab::ImportExport::WikiRestorer, Gitlab::ImportExport::WikiRestorer,
Gitlab::ImportExport::UploadsRestorer, Gitlab::ImportExport::UploadsRestorer,
Gitlab::ImportExport::LfsRestorer Gitlab::ImportExport::LfsRestorer,
Gitlab::ImportExport::StatisticsRestorer
].each do |restorer| ].each do |restorer|
it "calls the #{restorer}" do it "calls the #{restorer}" do
fake_restorer = double(restorer.to_s) fake_restorer = double(restorer.to_s)
...@@ -60,5 +63,42 @@ describe Gitlab::ImportExport::Importer do ...@@ -60,5 +63,42 @@ describe Gitlab::ImportExport::Importer do
importer.execute importer.execute
end end
end end
context 'when project successfully restored' do
let!(:existing_project) { create(:project, namespace: user.namespace) }
let(:project) { create(:project, namespace: user.namespace, name: 'whatever', path: 'whatever') }
before do
restorers = double
allow(subject).to receive(:import_file).and_return(true)
allow(subject).to receive(:check_version!).and_return(true)
allow(subject).to receive(:restorers).and_return(restorers)
allow(restorers).to receive(:all?).and_return(true)
allow(project).to receive(:import_data).and_return(double(data: { 'original_path' => existing_project.path }))
end
context 'when import_data' do
context 'has original_path' do
it 'overwrites existing project' do
expect_any_instance_of(::Projects::OverwriteProjectService).to receive(:execute).with(existing_project)
subject.execute
end
end
context 'has not original_path' do
before do
allow(project).to receive(:import_data).and_return(double(data: {}))
end
it 'does not call the overwrite service' do
expect_any_instance_of(::Projects::OverwriteProjectService).not_to receive(:execute).with(existing_project)
subject.execute
end
end
end
end
end end
end end
...@@ -16,7 +16,7 @@ describe Gitlab::Workhorse do ...@@ -16,7 +16,7 @@ describe Gitlab::Workhorse do
let(:ref) { 'master' } let(:ref) { 'master' }
let(:format) { 'zip' } let(:format) { 'zip' }
let(:storage_path) { Gitlab.config.gitlab.repository_downloads_path } let(:storage_path) { Gitlab.config.gitlab.repository_downloads_path }
let(:base_params) { repository.archive_metadata(ref, storage_path, format) } let(:base_params) { repository.archive_metadata(ref, storage_path, format, append_sha: nil) }
let(:gitaly_params) do let(:gitaly_params) do
base_params.merge( base_params.merge(
'GitalyServer' => { 'GitalyServer' => {
...@@ -29,7 +29,7 @@ describe Gitlab::Workhorse do ...@@ -29,7 +29,7 @@ describe Gitlab::Workhorse do
let(:cache_disabled) { false } let(:cache_disabled) { false }
subject do subject do
described_class.send_git_archive(repository, ref: ref, format: format) described_class.send_git_archive(repository, ref: ref, format: format, append_sha: nil)
end end
before do before do
......
...@@ -375,7 +375,8 @@ describe Environment do ...@@ -375,7 +375,8 @@ describe Environment do
context 'when there is a deployment platform for environment' do context 'when there is a deployment platform for environment' do
let!(:cluster) do let!(:cluster) do
create(:cluster, :provided_by_gcp, projects: [project]) create(:cluster, :provided_by_gcp,
environment_scope: '*', projects: [project])
end end
it 'finds a deployment platform' do it 'finds a deployment platform' do
......
...@@ -114,6 +114,29 @@ describe API::ProjectImport do ...@@ -114,6 +114,29 @@ describe API::ProjectImport do
expect(import_project.description).to eq('Hello world') expect(import_project.description).to eq('Hello world')
end end
context 'when target path already exists in namespace' do
let(:existing_project) { create(:project, namespace: user.namespace) }
it 'does not schedule an import' do
expect_any_instance_of(Project).not_to receive(:import_schedule)
post api('/projects/import', user), path: existing_project.path, file: fixture_file_upload(file)
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq('Name has already been taken')
end
context 'when param overwrite is true' do
it 'schedules an import' do
stub_import(user.namespace)
post api('/projects/import', user), path: existing_project.path, file: fixture_file_upload(file), overwrite: true
expect(response).to have_gitlab_http_status(201)
end
end
end
def stub_import(namespace) def stub_import(namespace)
expect_any_instance_of(Project).to receive(:import_schedule) expect_any_instance_of(Project).to receive(:import_schedule)
expect(::Projects::CreateService).to receive(:new).with(user, hash_including(namespace_id: namespace.id)).and_call_original expect(::Projects::CreateService).to receive(:new).with(user, hash_including(namespace_id: namespace.id)).and_call_original
......
...@@ -164,20 +164,36 @@ describe 'project routing' do ...@@ -164,20 +164,36 @@ describe 'project routing' do
# archive_project_repository GET /:project_id/repository/archive(.:format) projects/repositories#archive # archive_project_repository GET /:project_id/repository/archive(.:format) projects/repositories#archive
# edit_project_repository GET /:project_id/repository/edit(.:format) projects/repositories#edit # edit_project_repository GET /:project_id/repository/edit(.:format) projects/repositories#edit
describe Projects::RepositoriesController, 'routing' do describe Projects::RepositoriesController, 'routing' do
it 'to #archive' do
expect(get('/gitlab/gitlabhq/repository/master/archive')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', ref: 'master')
end
it 'to #archive format:zip' do it 'to #archive format:zip' do
expect(get('/gitlab/gitlabhq/repository/master/archive.zip')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'zip', ref: 'master') expect(get('/gitlab/gitlabhq/-/archive/master/archive.zip')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'zip', id: 'master/archive')
end end
it 'to #archive format:tar.bz2' do it 'to #archive format:tar.bz2' do
expect(get('/gitlab/gitlabhq/repository/master/archive.tar.bz2')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'tar.bz2', ref: 'master') expect(get('/gitlab/gitlabhq/-/archive/master/archive.tar.bz2')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'tar.bz2', id: 'master/archive')
end end
it 'to #archive with "/" in route' do it 'to #archive with "/" in route' do
expect(get('/gitlab/gitlabhq/repository/improve/awesome/archive')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', ref: 'improve/awesome') expect(get('/gitlab/gitlabhq/-/archive/improve/awesome/gitlabhq-improve-awesome.tar.gz')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'tar.gz', id: 'improve/awesome/gitlabhq-improve-awesome')
end
it 'to #archive_alternative' do
expect(get('/gitlab/gitlabhq/repository/archive')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', append_sha: true)
end
it 'to #archive_deprecated' do
expect(get('/gitlab/gitlabhq/repository/master/archive')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', append_sha: true)
end
it 'to #archive_deprecated format:zip' do
expect(get('/gitlab/gitlabhq/repository/master/archive.zip')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'zip', id: 'master', append_sha: true)
end
it 'to #archive_deprecated format:tar.bz2' do
expect(get('/gitlab/gitlabhq/repository/master/archive.tar.bz2')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'tar.bz2', id: 'master', append_sha: true)
end
it 'to #archive_deprecated with "/" in route' do
expect(get('/gitlab/gitlabhq/repository/improve/awesome/archive')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'improve/awesome', append_sha: true)
end end
end end
......
...@@ -261,6 +261,28 @@ describe Projects::DestroyService do ...@@ -261,6 +261,28 @@ describe Projects::DestroyService do
end end
end end
context '#attempt_restore_repositories' do
let(:path) { project.disk_path + '.git' }
before do
expect(project.gitlab_shell.exists?(project.repository_storage_path, path)).to be_truthy
expect(project.gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey
# Dont run sidekiq to check if renamed repository exists
Sidekiq::Testing.fake! { destroy_project(project, user, {}) }
expect(project.gitlab_shell.exists?(project.repository_storage_path, path)).to be_falsey
expect(project.gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_truthy
end
it 'restores the repositories' do
Sidekiq::Testing.fake! { described_class.new(project, user).attempt_repositories_rollback }
expect(project.gitlab_shell.exists?(project.repository_storage_path, path)).to be_truthy
expect(project.gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey
end
end
def destroy_project(project, user, params = {}) def destroy_project(project, user, params = {})
if async if async
Projects::DestroyService.new(project, user, params).async_execute Projects::DestroyService.new(project, user, params).async_execute
......
...@@ -4,7 +4,8 @@ describe Projects::GitlabProjectsImportService do ...@@ -4,7 +4,8 @@ describe Projects::GitlabProjectsImportService do
set(:namespace) { create(:namespace) } set(:namespace) { create(:namespace) }
let(:path) { 'test-path' } let(:path) { 'test-path' }
let(:file) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') } let(:file) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') }
let(:import_params) { { namespace_id: namespace.id, path: path, file: file } } let(:overwrite) { false }
let(:import_params) { { namespace_id: namespace.id, path: path, file: file, overwrite: overwrite } }
subject { described_class.new(namespace.owner, import_params) } subject { described_class.new(namespace.owner, import_params) }
describe '#execute' do describe '#execute' do
...@@ -37,5 +38,28 @@ describe Projects::GitlabProjectsImportService do ...@@ -37,5 +38,28 @@ describe Projects::GitlabProjectsImportService do
expect(project.import_data.data['override_params']['description']).to eq('Hello') expect(project.import_data.data['override_params']['description']).to eq('Hello')
end end
end end
context 'when there is a project with the same path' do
let(:existing_project) { create(:project, namespace: namespace) }
let(:path) { existing_project.path}
it 'does not create the project' do
project = subject.execute
expect(project).to be_invalid
expect(project).not_to be_persisted
end
context 'when overwrite param is set' do
let(:overwrite) { true }
it 'creates a project in a temporary full_path' do
project = subject.execute
expect(project).to be_valid
expect(project).to be_persisted
end
end
end
end end
end end
require 'spec_helper'
describe Projects::MoveAccessService do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project_with_access) { create(:project, namespace: user.namespace) }
let(:master_user) { create(:user) }
let(:reporter_user) { create(:user) }
let(:developer_user) { create(:user) }
let(:master_group) { create(:group) }
let(:reporter_group) { create(:group) }
let(:developer_group) { create(:group) }
before do
project_with_access.add_master(master_user)
project_with_access.add_developer(developer_user)
project_with_access.add_reporter(reporter_user)
project_with_access.project_group_links.create(group: master_group, group_access: Gitlab::Access::MASTER)
project_with_access.project_group_links.create(group: developer_group, group_access: Gitlab::Access::DEVELOPER)
project_with_access.project_group_links.create(group: reporter_group, group_access: Gitlab::Access::REPORTER)
end
subject { described_class.new(target_project, user) }
describe '#execute' do
shared_examples 'move the accesses' do
it do
expect(project_with_access.project_members.count).to eq 4
expect(project_with_access.project_group_links.count).to eq 3
expect(project_with_access.authorized_users.count).to eq 4
subject.execute(project_with_access)
expect(project_with_access.project_members.count).to eq 0
expect(project_with_access.project_group_links.count).to eq 0
expect(project_with_access.authorized_users.count).to eq 1
expect(target_project.project_members.count).to eq 4
expect(target_project.project_group_links.count).to eq 3
expect(target_project.authorized_users.count).to eq 4
end
it 'rollbacks if an exception is raised' do
allow(subject).to receive(:success).and_raise(StandardError)
expect { subject.execute(project_with_groups) }.to raise_error(StandardError)
expect(project_with_access.project_members.count).to eq 4
expect(project_with_access.project_group_links.count).to eq 3
expect(project_with_access.authorized_users.count).to eq 4
end
end
context 'when both projects are in the same namespace' do
let(:target_project) { create(:project, namespace: user.namespace) }
it 'does not refresh project owner authorized projects' do
allow(project_with_access).to receive(:namespace).and_return(user.namespace)
expect(project_with_access.namespace).not_to receive(:refresh_project_authorizations)
expect(target_project.namespace).not_to receive(:refresh_project_authorizations)
subject.execute(project_with_access)
end
it_behaves_like 'move the accesses'
end
context 'when projects are in different namespaces' do
let(:target_project) { create(:project, namespace: group) }
before do
group.add_owner(user)
end
it 'refreshes both project owner authorized projects' do
allow(project_with_access).to receive(:namespace).and_return(user.namespace)
expect(user.namespace).to receive(:refresh_project_authorizations).once
expect(group).to receive(:refresh_project_authorizations).once
subject.execute(project_with_access)
end
it_behaves_like 'move the accesses'
end
context 'when remove_remaining_elements is false' do
let(:target_project) { create(:project, namespace: user.namespace) }
let(:options) { { remove_remaining_elements: false } }
it 'does not remove remaining memberships' do
target_project.add_master(master_user)
subject.execute(project_with_access, options)
expect(project_with_access.project_members.count).not_to eq 0
end
it 'does not remove remaining group links' do
target_project.project_group_links.create(group: master_group, group_access: Gitlab::Access::MASTER)
subject.execute(project_with_access, options)
expect(project_with_access.project_group_links.count).not_to eq 0
end
it 'does not remove remaining authorizations' do
target_project.add_developer(developer_user)
subject.execute(project_with_access, options)
expect(project_with_access.project_authorizations.count).not_to eq 0
end
end
end
end
require 'spec_helper'
describe Projects::MoveDeployKeysProjectsService do
let!(:user) { create(:user) }
let!(:project_with_deploy_keys) { create(:project, namespace: user.namespace) }
let!(:target_project) { create(:project, namespace: user.namespace) }
subject { described_class.new(target_project, user) }
describe '#execute' do
before do
create_list(:deploy_keys_project, 2, project: project_with_deploy_keys)
end
it 'moves the user\'s deploy keys from one project to another' do
expect(project_with_deploy_keys.deploy_keys_projects.count).to eq 2
expect(target_project.deploy_keys_projects.count).to eq 0
subject.execute(project_with_deploy_keys)
expect(project_with_deploy_keys.deploy_keys_projects.count).to eq 0
expect(target_project.deploy_keys_projects.count).to eq 2
end
it 'does not link existent deploy_keys in the current project' do
target_project.deploy_keys << project_with_deploy_keys.deploy_keys.first
expect(project_with_deploy_keys.deploy_keys_projects.count).to eq 2
expect(target_project.deploy_keys_projects.count).to eq 1
subject.execute(project_with_deploy_keys)
expect(project_with_deploy_keys.deploy_keys_projects.count).to eq 0
expect(target_project.deploy_keys_projects.count).to eq 2
end
it 'rollbacks changes if transaction fails' do
allow(subject).to receive(:success).and_raise(StandardError)
expect { subject.execute(project_with_deploy_keys) }.to raise_error(StandardError)
expect(project_with_deploy_keys.deploy_keys_projects.count).to eq 2
expect(target_project.deploy_keys_projects.count).to eq 0
end
context 'when remove_remaining_elements is false' do
let(:options) { { remove_remaining_elements: false } }
it 'does not remove remaining deploy keys projects' do
target_project.deploy_keys << project_with_deploy_keys.deploy_keys.first
subject.execute(project_with_deploy_keys, options)
expect(project_with_deploy_keys.deploy_keys_projects.count).not_to eq 0
end
end
end
end
require 'spec_helper'
describe Projects::MoveForksService do
include ProjectForksHelper
let!(:user) { create(:user) }
let!(:project_with_forks) { create(:project, namespace: user.namespace) }
let!(:target_project) { create(:project, namespace: user.namespace) }
let!(:lvl1_forked_project_1) { fork_project(project_with_forks, user) }
let!(:lvl1_forked_project_2) { fork_project(project_with_forks, user) }
let!(:lvl2_forked_project_1_1) { fork_project(lvl1_forked_project_1, user) }
let!(:lvl2_forked_project_1_2) { fork_project(lvl1_forked_project_1, user) }
subject { described_class.new(target_project, user) }
describe '#execute' do
context 'when moving a root forked project' do
it 'moves the descendant forks' do
expect(project_with_forks.forks.count).to eq 2
expect(target_project.forks.count).to eq 0
subject.execute(project_with_forks)
expect(project_with_forks.forks.count).to eq 0
expect(target_project.forks.count).to eq 2
expect(lvl1_forked_project_1.forked_from_project).to eq target_project
expect(lvl1_forked_project_1.fork_network_member.forked_from_project).to eq target_project
expect(lvl1_forked_project_2.forked_from_project).to eq target_project
expect(lvl1_forked_project_2.fork_network_member.forked_from_project).to eq target_project
end
it 'updates the fork network' do
expect(project_with_forks.fork_network.root_project).to eq project_with_forks
expect(project_with_forks.fork_network.fork_network_members.map(&:project)).to include project_with_forks
subject.execute(project_with_forks)
expect(target_project.reload.fork_network.root_project).to eq target_project
expect(target_project.fork_network.fork_network_members.map(&:project)).not_to include project_with_forks
end
end
context 'when moving a intermediate forked project' do
it 'moves the descendant forks' do
expect(lvl1_forked_project_1.forks.count).to eq 2
expect(target_project.forks.count).to eq 0
subject.execute(lvl1_forked_project_1)
expect(lvl1_forked_project_1.forks.count).to eq 0
expect(target_project.forks.count).to eq 2
expect(lvl2_forked_project_1_1.forked_from_project).to eq target_project
expect(lvl2_forked_project_1_1.fork_network_member.forked_from_project).to eq target_project
expect(lvl2_forked_project_1_2.forked_from_project).to eq target_project
expect(lvl2_forked_project_1_2.fork_network_member.forked_from_project).to eq target_project
end
it 'moves the ascendant fork' do
subject.execute(lvl1_forked_project_1)
expect(target_project.forked_from_project).to eq project_with_forks
expect(target_project.fork_network_member.forked_from_project).to eq project_with_forks
end
it 'does not update fork network' do
subject.execute(lvl1_forked_project_1)
expect(target_project.reload.fork_network.root_project).to eq project_with_forks
end
end
context 'when moving a leaf forked project' do
it 'moves the ascendant fork' do
subject.execute(lvl2_forked_project_1_1)
expect(target_project.forked_from_project).to eq lvl1_forked_project_1
expect(target_project.fork_network_member.forked_from_project).to eq lvl1_forked_project_1
end
it 'does not update fork network' do
subject.execute(lvl2_forked_project_1_1)
expect(target_project.reload.fork_network.root_project).to eq project_with_forks
end
end
it 'rollbacks changes if transaction fails' do
allow(subject).to receive(:success).and_raise(StandardError)
expect { subject.execute(project_with_forks) }.to raise_error(StandardError)
expect(project_with_forks.forks.count).to eq 2
expect(target_project.forks.count).to eq 0
end
end
end
require 'spec_helper'
describe Projects::MoveLfsObjectsProjectsService do
let!(:user) { create(:user) }
let!(:project_with_lfs_objects) { create(:project, namespace: user.namespace) }
let!(:target_project) { create(:project, namespace: user.namespace) }
subject { described_class.new(target_project, user) }
before do
create_list(:lfs_objects_project, 3, project: project_with_lfs_objects)
end
describe '#execute' do
it 'links the lfs objects from existent in source project' do
expect(target_project.lfs_objects.count).to eq 0
subject.execute(project_with_lfs_objects)
expect(project_with_lfs_objects.reload.lfs_objects.count).to eq 0
expect(target_project.reload.lfs_objects.count).to eq 3
end
it 'does not link existent lfs_object in the current project' do
target_project.lfs_objects << project_with_lfs_objects.lfs_objects.first(2)
expect(target_project.lfs_objects.count).to eq 2
subject.execute(project_with_lfs_objects)
expect(target_project.lfs_objects.count).to eq 3
end
it 'rollbacks changes if transaction fails' do
allow(subject).to receive(:success).and_raise(StandardError)
expect { subject.execute(project_with_lfs_objects) }.to raise_error(StandardError)
expect(project_with_lfs_objects.lfs_objects.count).to eq 3
expect(target_project.lfs_objects.count).to eq 0
end
context 'when remove_remaining_elements is false' do
let(:options) { { remove_remaining_elements: false } }
it 'does not remove remaining lfs objects' do
target_project.lfs_objects << project_with_lfs_objects.lfs_objects.first(2)
subject.execute(project_with_lfs_objects, options)
expect(project_with_lfs_objects.lfs_objects.count).not_to eq 0
end
end
end
end
require 'spec_helper'
describe Projects::MoveNotificationSettingsService do
let(:user) { create(:user) }
let(:project_with_notifications) { create(:project, namespace: user.namespace) }
let(:target_project) { create(:project, namespace: user.namespace) }
subject { described_class.new(target_project, user) }
describe '#execute' do
context 'with notification settings' do
before do
create_list(:notification_setting, 2, source: project_with_notifications)
end
it 'moves the user\'s notification settings from one project to another' do
expect(project_with_notifications.notification_settings.count).to eq 3
expect(target_project.notification_settings.count).to eq 1
subject.execute(project_with_notifications)
expect(project_with_notifications.notification_settings.count).to eq 0
expect(target_project.notification_settings.count).to eq 3
end
it 'rollbacks changes if transaction fails' do
allow(subject).to receive(:success).and_raise(StandardError)
expect { subject.execute(project_with_notifications) }.to raise_error(StandardError)
expect(project_with_notifications.notification_settings.count).to eq 3
expect(target_project.notification_settings.count).to eq 1
end
end
it 'does not move existent notification settings in the current project' do
expect(project_with_notifications.notification_settings.count).to eq 1
expect(target_project.notification_settings.count).to eq 1
expect(user.notification_settings.count).to eq 2
subject.execute(project_with_notifications)
expect(user.notification_settings.count).to eq 1
end
context 'when remove_remaining_elements is false' do
let(:options) { { remove_remaining_elements: false } }
it 'does not remove remaining notification settings' do
subject.execute(project_with_notifications, options)
expect(project_with_notifications.notification_settings.count).not_to eq 0
end
end
end
end
require 'spec_helper'
describe Projects::MoveProjectAuthorizationsService do
let!(:user) { create(:user) }
let(:project_with_users) { create(:project, namespace: user.namespace) }
let(:target_project) { create(:project, namespace: user.namespace) }
let(:master_user) { create(:user) }
let(:reporter_user) { create(:user) }
let(:developer_user) { create(:user) }
subject { described_class.new(target_project, user) }
describe '#execute' do
before do
project_with_users.add_master(master_user)
project_with_users.add_developer(developer_user)
project_with_users.add_reporter(reporter_user)
end
it 'moves the authorizations from one project to another' do
expect(project_with_users.authorized_users.count).to eq 4
expect(target_project.authorized_users.count).to eq 1
subject.execute(project_with_users)
expect(project_with_users.authorized_users.count).to eq 0
expect(target_project.authorized_users.count).to eq 4
end
it 'does not move existent authorizations to the current project' do
target_project.add_master(developer_user)
target_project.add_developer(reporter_user)
expect(project_with_users.authorized_users.count).to eq 4
expect(target_project.authorized_users.count).to eq 3
subject.execute(project_with_users)
expect(project_with_users.authorized_users.count).to eq 0
expect(target_project.authorized_users.count).to eq 4
end
context 'when remove_remaining_elements is false' do
let(:options) { { remove_remaining_elements: false } }
it 'does not remove remaining project authorizations' do
target_project.add_master(developer_user)
target_project.add_developer(reporter_user)
subject.execute(project_with_users, options)
expect(project_with_users.project_authorizations.count).not_to eq 0
end
end
end
end
require 'spec_helper'
describe Projects::MoveProjectGroupLinksService do
let!(:user) { create(:user) }
let(:project_with_groups) { create(:project, namespace: user.namespace) }
let(:target_project) { create(:project, namespace: user.namespace) }
let(:master_group) { create(:group) }
let(:reporter_group) { create(:group) }
let(:developer_group) { create(:group) }
subject { described_class.new(target_project, user) }
describe '#execute' do
before do
project_with_groups.project_group_links.create(group: master_group, group_access: Gitlab::Access::MASTER)
project_with_groups.project_group_links.create(group: developer_group, group_access: Gitlab::Access::DEVELOPER)
project_with_groups.project_group_links.create(group: reporter_group, group_access: Gitlab::Access::REPORTER)
end
it 'moves the group links from one project to another' do
expect(project_with_groups.project_group_links.count).to eq 3
expect(target_project.project_group_links.count).to eq 0
subject.execute(project_with_groups)
expect(project_with_groups.project_group_links.count).to eq 0
expect(target_project.project_group_links.count).to eq 3
end
it 'does not move existent group links in the current project' do
target_project.project_group_links.create(group: master_group, group_access: Gitlab::Access::MASTER)
target_project.project_group_links.create(group: developer_group, group_access: Gitlab::Access::DEVELOPER)
expect(project_with_groups.project_group_links.count).to eq 3
expect(target_project.project_group_links.count).to eq 2
subject.execute(project_with_groups)
expect(project_with_groups.project_group_links.count).to eq 0
expect(target_project.project_group_links.count).to eq 3
end
it 'rollbacks changes if transaction fails' do
allow(subject).to receive(:success).and_raise(StandardError)
expect { subject.execute(project_with_groups) }.to raise_error(StandardError)
expect(project_with_groups.project_group_links.count).to eq 3
expect(target_project.project_group_links.count).to eq 0
end
context 'when remove_remaining_elements is false' do
let(:options) { { remove_remaining_elements: false } }
it 'does not remove remaining project group links' do
target_project.project_group_links.create(group: master_group, group_access: Gitlab::Access::MASTER)
target_project.project_group_links.create(group: developer_group, group_access: Gitlab::Access::DEVELOPER)
subject.execute(project_with_groups, options)
expect(project_with_groups.project_group_links.count).not_to eq 0
end
end
end
end
require 'spec_helper'
describe Projects::MoveProjectMembersService do
let!(:user) { create(:user) }
let(:project_with_users) { create(:project, namespace: user.namespace) }
let(:target_project) { create(:project, namespace: user.namespace) }
let(:master_user) { create(:user) }
let(:reporter_user) { create(:user) }
let(:developer_user) { create(:user) }
subject { described_class.new(target_project, user) }
describe '#execute' do
before do
project_with_users.add_master(master_user)
project_with_users.add_developer(developer_user)
project_with_users.add_reporter(reporter_user)
end
it 'moves the members from one project to another' do
expect(project_with_users.project_members.count).to eq 4
expect(target_project.project_members.count).to eq 1
subject.execute(project_with_users)
expect(project_with_users.project_members.count).to eq 0
expect(target_project.project_members.count).to eq 4
end
it 'does not move existent members to the current project' do
target_project.add_master(developer_user)
target_project.add_developer(reporter_user)
expect(project_with_users.project_members.count).to eq 4
expect(target_project.project_members.count).to eq 3
subject.execute(project_with_users)
expect(project_with_users.project_members.count).to eq 0
expect(target_project.project_members.count).to eq 4
end
it 'rollbacks changes if transaction fails' do
allow(subject).to receive(:success).and_raise(StandardError)
expect { subject.execute(project_with_users) }.to raise_error(StandardError)
expect(project_with_users.project_members.count).to eq 4
expect(target_project.project_members.count).to eq 1
end
context 'when remove_remaining_elements is false' do
let(:options) { { remove_remaining_elements: false } }
it 'does not remove remaining project members' do
target_project.add_master(developer_user)
target_project.add_developer(reporter_user)
subject.execute(project_with_users, options)
expect(project_with_users.project_members.count).not_to eq 0
end
end
end
end
require 'spec_helper'
describe Projects::MoveUsersStarProjectsService do
let!(:user) { create(:user) }
let!(:project_with_stars) { create(:project, namespace: user.namespace) }
let!(:target_project) { create(:project, namespace: user.namespace) }
subject { described_class.new(target_project, user) }
describe '#execute' do
before do
create_list(:users_star_project, 2, project: project_with_stars)
end
it 'moves the user\'s stars from one project to another' do
expect(project_with_stars.users_star_projects.count).to eq 2
expect(project_with_stars.star_count).to eq 2
expect(target_project.users_star_projects.count).to eq 0
expect(target_project.star_count).to eq 0
subject.execute(project_with_stars)
project_with_stars.reload
target_project.reload
expect(project_with_stars.users_star_projects.count).to eq 0
expect(project_with_stars.star_count).to eq 0
expect(target_project.users_star_projects.count).to eq 2
expect(target_project.star_count).to eq 2
end
it 'rollbacks changes if transaction fails' do
allow(subject).to receive(:success).and_raise(StandardError)
expect { subject.execute(project_with_stars) }.to raise_error(StandardError)
expect(project_with_stars.users_star_projects.count).to eq 2
expect(project_with_stars.star_count).to eq 2
expect(target_project.users_star_projects.count).to eq 0
expect(target_project.star_count).to eq 0
end
end
end
require 'spec_helper'
describe Projects::OverwriteProjectService do
include ProjectForksHelper
let(:user) { create(:user) }
let(:project_from) { create(:project, namespace: user.namespace) }
let(:project_to) { create(:project, namespace: user.namespace) }
let!(:lvl1_forked_project_1) { fork_project(project_from, user) }
let!(:lvl1_forked_project_2) { fork_project(project_from, user) }
let!(:lvl2_forked_project_1_1) { fork_project(lvl1_forked_project_1, user) }
let!(:lvl2_forked_project_1_2) { fork_project(lvl1_forked_project_1, user) }
subject { described_class.new(project_to, user) }
before do
allow(project_to).to receive(:import_data).and_return(double(data: { 'original_path' => project_from.path }))
end
describe '#execute' do
shared_examples 'overwrite actions' do
it 'moves deploy keys' do
deploy_keys_count = project_from.deploy_keys_projects.count
subject.execute(project_from)
expect(project_to.deploy_keys_projects.count).to eq deploy_keys_count
end
it 'moves notification settings' do
notification_count = project_from.notification_settings.count
subject.execute(project_from)
expect(project_to.notification_settings.count).to eq notification_count
end
it 'moves users stars' do
stars_count = project_from.users_star_projects.count
subject.execute(project_from)
project_to.reload
expect(project_to.users_star_projects.count).to eq stars_count
expect(project_to.star_count).to eq stars_count
end
it 'moves project group links' do
group_links_count = project_from.project_group_links.count
subject.execute(project_from)
expect(project_to.project_group_links.count).to eq group_links_count
end
it 'moves memberships and authorizations' do
members_count = project_from.project_members.count
project_authorizations = project_from.project_authorizations.count
subject.execute(project_from)
expect(project_to.project_members.count).to eq members_count
expect(project_to.project_authorizations.count).to eq project_authorizations
end
context 'moves lfs objects relationships' do
before do
create_list(:lfs_objects_project, 3, project: project_from)
end
it do
lfs_objects_count = project_from.lfs_objects.count
subject.execute(project_from)
expect(project_to.lfs_objects.count).to eq lfs_objects_count
end
end
it 'removes the original project' do
subject.execute(project_from)
expect { Project.find(project_from.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
it 'renames the project' do
subject.execute(project_from)
expect(project_to.full_path).to eq project_from.full_path
end
end
context 'when project does not have any relation' do
it_behaves_like 'overwrite actions'
end
context 'when project with elements' do
it_behaves_like 'overwrite actions' do
let(:master_user) { create(:user) }
let(:reporter_user) { create(:user) }
let(:developer_user) { create(:user) }
let(:master_group) { create(:group) }
let(:reporter_group) { create(:group) }
let(:developer_group) { create(:group) }
before do
create_list(:deploy_keys_project, 2, project: project_from)
create_list(:notification_setting, 2, source: project_from)
create_list(:users_star_project, 2, project: project_from)
project_from.project_group_links.create(group: master_group, group_access: Gitlab::Access::MASTER)
project_from.project_group_links.create(group: developer_group, group_access: Gitlab::Access::DEVELOPER)
project_from.project_group_links.create(group: reporter_group, group_access: Gitlab::Access::REPORTER)
project_from.add_master(master_user)
project_from.add_developer(developer_user)
project_from.add_reporter(reporter_user)
end
end
end
context 'forks' do
context 'when moving a root forked project' do
it 'moves the descendant forks' do
expect(project_from.forks.count).to eq 2
expect(project_to.forks.count).to eq 0
subject.execute(project_from)
expect(project_from.forks.count).to eq 0
expect(project_to.forks.count).to eq 2
expect(lvl1_forked_project_1.forked_from_project).to eq project_to
expect(lvl1_forked_project_1.fork_network_member.forked_from_project).to eq project_to
expect(lvl1_forked_project_2.forked_from_project).to eq project_to
expect(lvl1_forked_project_2.fork_network_member.forked_from_project).to eq project_to
end
it 'updates the fork network' do
expect(project_from.fork_network.root_project).to eq project_from
expect(project_from.fork_network.fork_network_members.map(&:project)).to include project_from
subject.execute(project_from)
expect(project_to.reload.fork_network.root_project).to eq project_to
expect(project_to.fork_network.fork_network_members.map(&:project)).not_to include project_from
end
end
context 'when moving a intermediate forked project' do
let(:project_to) { create(:project, namespace: lvl1_forked_project_1.namespace) }
it 'moves the descendant forks' do
expect(lvl1_forked_project_1.forks.count).to eq 2
expect(project_to.forks.count).to eq 0
subject.execute(lvl1_forked_project_1)
expect(lvl1_forked_project_1.forks.count).to eq 0
expect(project_to.forks.count).to eq 2
expect(lvl2_forked_project_1_1.forked_from_project).to eq project_to
expect(lvl2_forked_project_1_1.fork_network_member.forked_from_project).to eq project_to
expect(lvl2_forked_project_1_2.forked_from_project).to eq project_to
expect(lvl2_forked_project_1_2.fork_network_member.forked_from_project).to eq project_to
end
it 'moves the ascendant fork' do
subject.execute(lvl1_forked_project_1)
expect(project_to.reload.forked_from_project).to eq project_from
expect(project_to.fork_network_member.forked_from_project).to eq project_from
end
it 'does not update fork network' do
subject.execute(lvl1_forked_project_1)
expect(project_to.reload.fork_network.root_project).to eq project_from
end
end
end
context 'if an exception is raised' do
it 'rollbacks changes' do
updated_at = project_from.updated_at
allow(subject).to receive(:rename_project).and_raise(StandardError)
expect { subject.execute(project_from) }.to raise_error(StandardError)
expect(Project.find(project_from.id)).not_to be_nil
expect(project_from.reload.updated_at.change(usec: 0)).to eq updated_at.change(usec: 0)
end
it 'tries to restore the original project repositories' do
allow(subject).to receive(:rename_project).and_raise(StandardError)
expect(subject).to receive(:attempt_restore_repositories).with(project_from)
expect { subject.execute(project_from) }.to raise_error(StandardError)
end
end
end
end
...@@ -28,6 +28,6 @@ describe 'projects/merge_requests/_commits.html.haml' do ...@@ -28,6 +28,6 @@ describe 'projects/merge_requests/_commits.html.haml' do
commit = merge_request.commits.first # HEAD commit = merge_request.commits.first # HEAD
href = diffs_project_merge_request_path(target_project, merge_request, commit_id: commit) href = diffs_project_merge_request_path(target_project, merge_request, commit_id: commit)
expect(rendered).to have_link(Commit.truncate_sha(commit.sha), href: href) expect(rendered).to have_link(href: href)
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment