Commit 402b651a authored by Douwe Maan's avatar Douwe Maan

Merge branch 'feature/project-export-ui-experimental' into 'feature/project-import'

Experimental UI for exporting and importing a project


Part of https://gitlab.com/gitlab-org/gitlab-ce/issues/3050

Screenshots of both the export and import processes:

## Export
1 -  Project settings

 ![Screen_Shot_2016-06-16_at_15.29.27](/uploads/ec59113dae9132e594b79289e3598f5c/Screen_Shot_2016-06-16_at_15.29.27.png)

2 -  Flash after clicking on export

![Screen_Shot_2016-06-16_at_15.29.47](/uploads/02f20d1500de4e0c9693218f9fb2c414/Screen_Shot_2016-06-16_at_15.29.47.png)

3 - Email received with download link

![Screen_Shot_2016-06-16_at_15.36.19](/uploads/0f7e0a74125d9f1fa067eb52104397a5/Screen_Shot_2016-06-16_at_15.36.19.png)

4 - The project settings export screen changes so we can either delete the file or download it again (it won't generate a new export, unless we delete it first)

![Screen_Shot_2016-06-16_at_15.28.43](/uploads/073f87cc751a857eac94e55d1d0c4ef9/Screen_Shot_2016-06-16_at_15.28.43.png)

5 - After delete flash

![Screen_Shot_2016-06-16_at_15.29.10](/uploads/e80341aebcaed8f7713868793fda2b92/Screen_Shot_2016-06-16_at_15.29.10.png)

## Import
1 - New project page with new gitlab export option

![Screen_Shot_2016-06-16_at_15.31.25](/uploads/246e823a52c5b0216354c4f5321f846b/Screen_Shot_2016-06-16_at_15.31.25.png)

2 - Next step importing - choosing a file

![Screen_Shot_2016-06-16_at_15.32.23](/uploads/f91e72f68cc844577a0fc1935e3936d3/Screen_Shot_2016-06-16_at_15.32.23.png)

3 - Import in progress

![Screen_Shot_2016-06-16_at_15.32.48](/uploads/41c774c0c03a91b60cd220ce77cab8e6/Screen_Shot_2016-06-16_at_15.32.48.png)

4 - Import successful 

![Screen_Shot_2016-06-16_at_15.32.54](/uploads/337f9a07779999d00232f7ac61ed362b/Screen_Shot_2016-06-16_at_15.32.54.png)



See merge request !4012
parents 2a747d38 2d4556c5
...@@ -90,6 +90,7 @@ v 8.9.0 (unreleased) ...@@ -90,6 +90,7 @@ v 8.9.0 (unreleased)
- Improved UX of date pickers on issue & milestone forms - Improved UX of date pickers on issue & milestone forms
- Cache on the database if a project has an active external issue tracker. - Cache on the database if a project has an active external issue tracker.
- Put project Labels and Milestones pages links under Issues and Merge Requests tabs as subnav - Put project Labels and Milestones pages links under Issues and Merge Requests tabs as subnav
- GitLab project import and export functionality
- All classes in the Banzai::ReferenceParser namespace are now instrumented - All classes in the Banzai::ReferenceParser namespace are now instrumented
- Remove deprecated issues_tracker and issues_tracker_id from project model - Remove deprecated issues_tracker and issues_tracker_id from project model
- Allow users to create confidential issues in private projects - Allow users to create confidential issues in private projects
......
...@@ -221,7 +221,7 @@ gem 'jquery-turbolinks', '~> 2.1.0' ...@@ -221,7 +221,7 @@ gem 'jquery-turbolinks', '~> 2.1.0'
gem 'addressable', '~> 2.3.8' gem 'addressable', '~> 2.3.8'
gem 'bootstrap-sass', '~> 3.3.0' gem 'bootstrap-sass', '~> 3.3.0'
gem 'font-awesome-rails', '~> 4.2' gem 'font-awesome-rails', '~> 4.6.1'
gem 'gitlab_emoji', '~> 0.3.0' gem 'gitlab_emoji', '~> 0.3.0'
gem 'gon', '~> 6.0.1' gem 'gon', '~> 6.0.1'
gem 'jquery-atwho-rails', '~> 1.3.2' gem 'jquery-atwho-rails', '~> 1.3.2'
......
...@@ -246,7 +246,7 @@ GEM ...@@ -246,7 +246,7 @@ GEM
fog-xml (0.1.2) fog-xml (0.1.2)
fog-core fog-core
nokogiri (~> 1.5, >= 1.5.11) nokogiri (~> 1.5, >= 1.5.11)
font-awesome-rails (4.5.0.1) font-awesome-rails (4.6.1.0)
railties (>= 3.2, < 5.1) railties (>= 3.2, < 5.1)
foreman (0.78.0) foreman (0.78.0)
thor (~> 0.19.1) thor (~> 0.19.1)
...@@ -866,7 +866,7 @@ DEPENDENCIES ...@@ -866,7 +866,7 @@ DEPENDENCIES
fog-google (~> 0.3) fog-google (~> 0.3)
fog-local (~> 0.3) fog-local (~> 0.3)
fog-openstack (~> 0.1) fog-openstack (~> 0.1)
font-awesome-rails (~> 4.2) font-awesome-rails (~> 4.6.1)
foreman foreman
fuubar (~> 2.0.0) fuubar (~> 2.0.0)
gemnasium-gitlab-service (~> 0.2) gemnasium-gitlab-service (~> 0.2)
......
...@@ -24,7 +24,7 @@ class ApplicationController < ActionController::Base ...@@ -24,7 +24,7 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception protect_from_forgery with: :exception
helper_method :abilities, :can?, :current_application_settings helper_method :abilities, :can?, :current_application_settings
helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled? helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?
rescue_from Encoding::CompatibilityError do |exception| rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception) log_exception(exception)
...@@ -326,6 +326,10 @@ class ApplicationController < ActionController::Base ...@@ -326,6 +326,10 @@ class ApplicationController < ActionController::Base
current_application_settings.import_sources.include?('git') current_application_settings.import_sources.include?('git')
end end
def gitlab_project_import_enabled?
current_application_settings.import_sources.include?('gitlab_project')
end
def two_factor_authentication_required? def two_factor_authentication_required?
current_application_settings.require_two_factor_authentication current_application_settings.require_two_factor_authentication
end end
......
class Import::GitlabProjectsController < Import::BaseController
before_action :verify_gitlab_project_import_enabled
def new
@namespace_id = project_params[:namespace_id]
@namespace_name = Namespace.find(project_params[:namespace_id]).name
@path = project_params[:path]
end
def create
unless file_is_valid?
return redirect_back_or_default(options: { alert: "You need to upload a GitLab project export archive." })
end
@project = Gitlab::ImportExport::ProjectCreator.new(project_params[:namespace_id],
current_user,
File.expand_path(project_params[:file].path),
project_params[:path]).execute
if @project.saved?
redirect_to(
project_path(@project),
notice: "Project '#{@project.name}' is being imported."
)
else
redirect_to(
new_import_gitlab_project_path,
alert: "Project could not be imported: #{@project.errors.full_messages.join(', ')}"
)
end
end
private
def file_is_valid?
project_params[:file] && project_params[:file].respond_to?(:read)
end
def verify_gitlab_project_import_enabled
render_404 unless gitlab_project_import_enabled?
end
def project_params
params.permit(
:path, :namespace_id, :file
)
end
end
...@@ -7,7 +7,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -7,7 +7,7 @@ class ProjectsController < Projects::ApplicationController
before_action :assign_ref_vars, :tree, only: [:show], if: :repo_exists? before_action :assign_ref_vars, :tree, only: [:show], if: :repo_exists?
# Authorize # Authorize
before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping] before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping, :download_export, :export, :remove_export, :generate_new_export]
before_action :event_filter, only: [:show, :activity] before_action :event_filter, only: [:show, :activity]
layout :determine_layout layout :determine_layout
...@@ -185,6 +185,48 @@ class ProjectsController < Projects::ApplicationController ...@@ -185,6 +185,48 @@ class ProjectsController < Projects::ApplicationController
) )
end end
def export
@project.add_export_job(current_user: current_user)
redirect_to(
edit_project_path(@project),
notice: "Project export started. A download link will be sent by email."
)
end
def download_export
export_project_path = @project.export_project_path
if export_project_path
send_file export_project_path, disposition: 'attachment'
else
redirect_to(
edit_project_path(@project),
alert: "Project export link has expired. Please generate a new export from your project settings."
)
end
end
def remove_export
if @project.remove_exports
flash[:notice] = "Project export has been deleted."
else
flash[:alert] = "Project export could not be deleted."
end
redirect_to(edit_project_path(@project))
end
def generate_new_export
if @project.remove_exports
export
else
redirect_to(
edit_project_path(@project),
alert: "Project export could not be deleted."
)
end
end
def toggle_star def toggle_star
current_user.toggle_star(@project) current_user.toggle_star(@project)
@project.reload @project.reload
......
...@@ -9,6 +9,19 @@ module Emails ...@@ -9,6 +9,19 @@ module Emails
subject: subject("Project was moved")) subject: subject("Project was moved"))
end end
def project_was_exported_email(current_user, project)
@project = project
mail(to: current_user.notification_email,
subject: subject("Project was exported"))
end
def project_was_not_exported_email(current_user, project, errors)
@project = project
@errors = errors
mail(to: current_user.notification_email,
subject: subject("Project export error"))
end
def repository_push_email(project_id, opts = {}) def repository_push_email(project_id, opts = {})
@message = @message =
Gitlab::Email::Message::RepositoryPush.new(self, project_id, opts) Gitlab::Email::Message::RepositoryPush.new(self, project_id, opts)
......
...@@ -123,7 +123,7 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -123,7 +123,7 @@ class ApplicationSetting < ActiveRecord::Base
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
restricted_signup_domains: Settings.gitlab['restricted_signup_domains'], restricted_signup_domains: Settings.gitlab['restricted_signup_domains'],
import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'], import_sources: %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project],
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'], max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false, require_two_factor_authentication: false,
......
...@@ -352,14 +352,9 @@ class Project < ActiveRecord::Base ...@@ -352,14 +352,9 @@ class Project < ActiveRecord::Base
joins(join_body).reorder('join_note_counts.amount DESC') joins(join_body).reorder('join_note_counts.amount DESC')
end end
def create_from_import_job(current_user_id:, tmp_file:, namespace_id:, project_path:) # Deletes gitlab project export files older than 24 hours
job_id = ProjectImportWorker.perform_async(current_user_id, tmp_file, namespace_id, project_path) def remove_gitlab_exports!
Gitlab::Popen.popen(%W(find #{Gitlab::ImportExport.storage_path} -not -path #{Gitlab::ImportExport.storage_path} -mmin +1440 -delete))
if job_id
Rails.logger.info "Import job started for export #{tmp_file} with job ID #{job_id}"
else
Rails.logger.error "Import job failed to start for #{tmp_file}"
end
end end
end end
...@@ -464,7 +459,7 @@ class Project < ActiveRecord::Base ...@@ -464,7 +459,7 @@ class Project < ActiveRecord::Base
end end
def import? def import?
external_import? || forked? external_import? || forked? || gitlab_project_import?
end end
def no_import? def no_import?
...@@ -495,6 +490,10 @@ class Project < ActiveRecord::Base ...@@ -495,6 +490,10 @@ class Project < ActiveRecord::Base
Gitlab::UrlSanitizer.new(import_url).masked_url Gitlab::UrlSanitizer.new(import_url).masked_url
end end
def gitlab_project_import?
import_type == 'gitlab_project'
end
def check_limit def check_limit
unless creator.can_create_project? or namespace.kind == 'group' unless creator.can_create_project? or namespace.kind == 'group'
projects_limit = creator.projects_limit projects_limit = creator.projects_limit
...@@ -1091,8 +1090,8 @@ class Project < ActiveRecord::Base ...@@ -1091,8 +1090,8 @@ class Project < ActiveRecord::Base
@errors = original_errors @errors = original_errors
end end
def add_export_job(current_user_id:) def add_export_job(current_user:)
job_id = ProjectExportWorker.perform_async(current_user_id, self.id) job_id = ProjectExportWorker.perform_async(current_user.id, self.id)
if job_id if job_id
Rails.logger.info "Export job started for project ID #{self.id} with job ID #{job_id}" Rails.logger.info "Export job started for project ID #{self.id} with job ID #{job_id}"
...@@ -1100,4 +1099,17 @@ class Project < ActiveRecord::Base ...@@ -1100,4 +1099,17 @@ class Project < ActiveRecord::Base
Rails.logger.error "Export job failed to start for project ID #{self.id}" Rails.logger.error "Export job failed to start for project ID #{self.id}"
end end
end end
def export_path
File.join(Gitlab::ImportExport.storage_path, path_with_namespace)
end
def export_project_path
Dir.glob("#{export_path}/*export.tar.gz").max_by { |f| File.ctime(f) }
end
def remove_exports
_, status = Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete))
status.zero?
end
end end
...@@ -266,6 +266,14 @@ class NotificationService ...@@ -266,6 +266,14 @@ class NotificationService
end end
end end
def project_exported(project, current_user)
mailer.project_was_exported_email(current_user, project).deliver_later
end
def project_not_exported(project, current_user, errors)
mailer.project_was_not_exported_email(current_user, project, errors).deliver_later
end
protected protected
# Get project users with WATCH notification level # Get project users with WATCH notification level
......
...@@ -80,16 +80,18 @@ module Projects ...@@ -80,16 +80,18 @@ module Projects
def after_create_actions def after_create_actions
log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"") log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"")
unless @project.gitlab_project_import?
@project.create_wiki if @project.wiki_enabled? @project.create_wiki if @project.wiki_enabled?
@project.build_missing_services @project.build_missing_services
@project.create_labels @project.create_labels
end
event_service.create_project(@project, current_user) event_service.create_project(@project, current_user)
system_hook_service.execute_hooks_for(@project, :create) system_hook_service.execute_hooks_for(@project, :create)
unless @project.group unless @project.group || @project.gitlab_project_import?
@project.team << [current_user, :master, current_user] @project.team << [current_user, :master, current_user]
end end
end end
......
...@@ -12,8 +12,9 @@ module Projects ...@@ -12,8 +12,9 @@ module Projects
def save_all def save_all
if [version_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver].all?(&:save) if [version_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver].all?(&:save)
Gitlab::ImportExport::Saver.save(shared: @shared) Gitlab::ImportExport::Saver.save(shared: @shared)
notify_success
else else
cleanup_and_notify_worker cleanup_and_notify
end end
end end
...@@ -37,10 +38,20 @@ module Projects ...@@ -37,10 +38,20 @@ module Projects
Gitlab::ImportExport::WikiRepoSaver.new(project: project, shared: @shared) Gitlab::ImportExport::WikiRepoSaver.new(project: project, shared: @shared)
end end
def cleanup_and_notify_worker def cleanup_and_notify
FileUtils.rm_rf(@shared.export_path) FileUtils.rm_rf(@shared.export_path)
notify_error
raise Gitlab::ImportExport::Error.new(@shared.errors.join(', ')) raise Gitlab::ImportExport::Error.new(@shared.errors.join(', '))
end end
def notify_success
notification_service.project_exported(@project, @current_user)
end
def notify_error
notification_service.project_not_exported(@project, @current_user, @shared.errors.join(', '))
end
end end
end end
end end
...@@ -9,26 +9,31 @@ module Projects ...@@ -9,26 +9,31 @@ module Projects
'fogbugz', 'fogbugz',
'gitlab', 'gitlab',
'github', 'github',
'google_code' 'google_code',
'gitlab_project'
] ]
def execute def execute
if unknown_url? add_repository_to_project unless project.gitlab_project_import?
# In this case, we only want to import issues, not a repository.
create_repository
else
import_repository
end
import_data import_data
success success
rescue Error => e rescue => e
error(e.message) error(e.message)
end end
private private
def add_repository_to_project
if unknown_url?
# In this case, we only want to import issues, not a repository.
create_repository
else
import_repository
end
end
def create_repository def create_repository
unless project.create_repository unless project.create_repository
raise Error, 'The repository could not be created.' raise Error, 'The repository could not be created.'
...@@ -46,7 +51,7 @@ module Projects ...@@ -46,7 +51,7 @@ module Projects
def import_data def import_data
return unless has_importer? return unless has_importer?
project.repository.before_import project.repository.before_import unless project.gitlab_project_import?
unless importer.execute unless importer.execute
raise Error, 'The remote data could not be imported.' raise Error, 'The remote data could not be imported.'
...@@ -58,6 +63,8 @@ module Projects ...@@ -58,6 +63,8 @@ module Projects
end end
def importer def importer
return Gitlab::ImportExport::Importer.new(project) if @project.gitlab_project_import?
class_name = "Gitlab::#{project.import_type.camelize}Import::Importer" class_name = "Gitlab::#{project.import_type.camelize}Import::Importer"
class_name.constantize.new(project) class_name.constantize.new(project)
end end
......
- page_title "GitLab Import"
- header_title "Projects", root_path
%h3.page-title
= icon('gitlab')
Import an exported GitLab project
%hr
= form_tag import_gitlab_project_path, class: 'form-horizontal', multipart: true do
%p
Project will be imported as
%strong
#{@namespace_name}/#{@path}
%p
To move or copy an entire GitLab project from another GitLab installation to this one, navigate to the original project's settings page, generate an export file, and upload it here.
.form-group
= hidden_field_tag :namespace_id, @namespace_id
= hidden_field_tag :path, @path
= label_tag :file, class: 'control-label' do
%span GitLab project export
.col-sm-10
= file_field_tag :file, class: ''
.form-actions
= submit_tag 'Import project', class: 'btn btn-create'
%p
Project #{@project.name} was exported successfully.
%p
The project export can be downloaded from:
= link_to download_export_namespace_project_url(@project.namespace, @project) do
= @project.name_with_namespace + " export"
%p
The download link will expire in 24 hours.
Project <%= @project.name %> was exported successfully.
The project export can be downloaded from:
<%= download_export_namespace_project_url(@project.namespace, @project) %>
The download link will expire in 24 hours.
%p
Project #{@project.name} couldn't be exported.
%p
The errors we encountered were:
%ul
- @errors.each do |error|
%li
error
Project <%= @project.name %> couldn't be exported.
The errors we encountered were:
- @errors.each do |error|
<%= error %>
\ No newline at end of file
...@@ -120,6 +120,42 @@ ...@@ -120,6 +120,42 @@
= link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project), = link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project),
method: :post, class: "btn btn-save" method: :post, class: "btn btn-save"
%hr %hr
.row.prepend-top-default
.col-lg-3
%h4.prepend-top-0
Export project
%p.append-bottom-0
%p
Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page.
%p
Once the exported file is ready, you will receive a notification email with a download link.
.col-lg-9
- if @project.export_project_path
= link_to 'Download export', download_export_namespace_project_path(@project.namespace, @project),
method: :get, class: "btn btn-default"
= link_to 'Generate new export', generate_new_export_namespace_project_path(@project.namespace, @project),
method: :post, class: "btn btn-default"
- else
= link_to 'Export project', export_namespace_project_path(@project.namespace, @project),
method: :post, class: "btn btn-default"
.bs-callout.bs-callout-info
%p.append-bottom-0
%p
The following items will be exported:
%ul
%li Project and wiki repositories
%li Project uploads
%li Project configuration including web hooks and services
%li Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities
%p
The following items will NOT be exported:
%ul
%li Build traces and artifacts
%li LFS objects
%hr
- if can? current_user, :archive_project, @project - if can? current_user, :archive_project, @project
.row.prepend-top-default .row.prepend-top-default
.col-lg-3 .col-lg-3
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
Forking in progress. Forking in progress.
- else - else
Import in progress. Import in progress.
- unless @project.forked? - if @project.external_import?
%p.monospace git clone --bare #{@project.safe_import_url} %p.monospace git clone --bare #{@project.safe_import_url}
%p Please wait while we import the repository for you. Refresh at will. %p Please wait while we import the repository for you. Refresh at will.
:javascript :javascript
......
...@@ -84,7 +84,12 @@ ...@@ -84,7 +84,12 @@
- if git_import_enabled? - if git_import_enabled?
= link_to "#", class: 'btn js-toggle-button import_git' do = link_to "#", class: 'btn js-toggle-button import_git' do
%i.fa.fa-git %i.fa.fa-git
%span Any repo by URL %span Repo by URL
- if gitlab_project_import_enabled?
= link_to new_import_gitlab_project_path, class: 'btn import_gitlab_project project-submit' do
%i.fa.fa-gitlab
%span GitLab export
.js-toggle-content.hide .js-toggle-content.hide
= render "shared/import_form", f: f = render "shared/import_form", f: f
...@@ -115,6 +120,33 @@ ...@@ -115,6 +120,33 @@
e.preventDefault(); e.preventDefault();
var import_modal = $(this).next(".modal").show(); var import_modal = $(this).next(".modal").show();
}); });
$('.modal-header .close').bind('click', function() { $('.modal-header .close').bind('click', function() {
$(".modal").hide(); $(".modal").hide();
}); });
$('.import_gitlab_project').bind('click', function() {
var _href = $("a.import_gitlab_project").attr("href");
$(".import_gitlab_project").attr("href", _href + '?namespace_id=' + $("#project_namespace_id").val() + '&path=' + $("#project_path").val());
});
$('.import_gitlab_project').attr('disabled',true)
$('.import_gitlab_project').attr('title', 'Project path required.');
$('.import_gitlab_project').click(function( event ) {
if($('.import_gitlab_project').attr('disabled')) {
event.preventDefault();
new Flash("Please enter a path for the project to be imported to.");
}
});
$('#project_path').keyup(function(){
if($(this).val().length !=0) {
$('.import_gitlab_project').attr('disabled', false);
$('.import_gitlab_project').attr('title','');
$(".flash-container").html("")
} else {
$('.import_gitlab_project').attr('disabled',true);
$('.import_gitlab_project').attr('title', 'Project path required.');
}
})
class GitlabRemoveProjectExportWorker
include Sidekiq::Worker
sidekiq_options queue: :default
def perform
Project.remove_gitlab_exports!
end
end
class ProjectExportWorker class ProjectExportWorker
include Sidekiq::Worker include Sidekiq::Worker
# TODO: enable retry - disabled for QA purposes sidekiq_options queue: :gitlab_shell, retry: true
sidekiq_options queue: :gitlab_shell, retry: false
def perform(current_user_id, project_id) def perform(current_user_id, project_id)
current_user = User.find(current_user_id) current_user = User.find(current_user_id)
project = Project.find(project_id) project = Project.find(project_id)
::Projects::ImportExport::ExportService.new(project, current_user).execute ::Projects::ImportExport::ExportService.new(project, current_user).execute
end end
end end
class ProjectImportWorker
include Sidekiq::Worker
# TODO: enabled retry - disabled for QA purposes
sidekiq_options queue: :gitlab_shell, retry: false
def perform(current_user_id, tmp_file, namespace_id, path)
current_user = User.find(current_user_id)
project = Gitlab::ImportExport::ImportService.execute(archive_file: tmp_file,
owner: current_user,
namespace_id: namespace_id,
project_path: path)
if project
project.repository.after_import
else
logger.error("There was an error during the import: #{tmp_file}")
end
end
def logger
Sidekiq.logger
end
end
...@@ -291,6 +291,9 @@ Settings.cron_jobs['admin_email_worker']['job_class'] = 'AdminEmailWorker' ...@@ -291,6 +291,9 @@ Settings.cron_jobs['admin_email_worker']['job_class'] = 'AdminEmailWorker'
Settings.cron_jobs['repository_archive_cache_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['repository_archive_cache_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['repository_archive_cache_worker']['cron'] ||= '0 * * * *' Settings.cron_jobs['repository_archive_cache_worker']['cron'] ||= '0 * * * *'
Settings.cron_jobs['repository_archive_cache_worker']['job_class'] = 'RepositoryArchiveCacheWorker' Settings.cron_jobs['repository_archive_cache_worker']['job_class'] = 'RepositoryArchiveCacheWorker'
Settings.cron_jobs['gitlab_remove_project_export_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['gitlab_remove_project_export_worker']['cron'] ||= '0 * * * *'
Settings.cron_jobs['gitlab_remove_project_export_worker']['job_class'] = 'GitlabRemoveProjectExportWorker'
# #
# GitLab Shell # GitLab Shell
......
...@@ -459,6 +459,10 @@ Rails.application.routes.draw do ...@@ -459,6 +459,10 @@ Rails.application.routes.draw do
post :housekeeping post :housekeeping
post :toggle_star post :toggle_star
post :markdown_preview post :markdown_preview
post :export
post :remove_export
post :generate_new_export
get :download_export
get :autocomplete_sources get :autocomplete_sources
get :activity get :activity
end end
......
...@@ -14,7 +14,7 @@ Background: ...@@ -14,7 +14,7 @@ Background:
@javascript @javascript
Scenario: I should see instructions on how to import from Git URL Scenario: I should see instructions on how to import from Git URL
Given I see "New Project" page Given I see "New Project" page
When I click on "Any repo by URL" When I click on "Repo by URL"
Then I see instructions on how to import from Git URL Then I see instructions on how to import from Git URL
@javascript @javascript
......
...@@ -20,7 +20,8 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps ...@@ -20,7 +20,8 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
expect(page).to have_link('GitLab.com') expect(page).to have_link('GitLab.com')
expect(page).to have_link('Gitorious.org') expect(page).to have_link('Gitorious.org')
expect(page).to have_link('Google Code') expect(page).to have_link('Google Code')
expect(page).to have_link('Any repo by URL') expect(page).to have_link('Repo by URL')
expect(page).to have_link('GitLab export')
end end
step 'I click on "Import project from GitHub"' do step 'I click on "Import project from GitHub"' do
...@@ -37,7 +38,7 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps ...@@ -37,7 +38,7 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
end end
end end
step 'I click on "Any repo by URL"' do step 'I click on "Repo by URL"' do
first('.import_git').click first('.import_git').click
end end
......
...@@ -36,7 +36,7 @@ module Gitlab ...@@ -36,7 +36,7 @@ module Gitlab
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
restricted_signup_domains: Settings.gitlab['restricted_signup_domains'], restricted_signup_domains: Settings.gitlab['restricted_signup_domains'],
import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'], import_sources: %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project],
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'], max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false, require_two_factor_authentication: false,
......
...@@ -11,7 +11,7 @@ module Gitlab ...@@ -11,7 +11,7 @@ module Gitlab
end end
def execute def execute
project = ::Projects::CreateService.new( ::Projects::CreateService.new(
current_user, current_user,
name: repo["name"], name: repo["name"],
path: repo["path"], path: repo["path"],
...@@ -22,8 +22,6 @@ module Gitlab ...@@ -22,8 +22,6 @@ module Gitlab
import_source: repo["path_with_namespace"], import_source: repo["path_with_namespace"],
import_url: repo["http_url_to_repo"].sub("://", "://oauth2:#{@session_data[:gitlab_access_token]}@") import_url: repo["http_url_to_repo"].sub("://", "://oauth2:#{@session_data[:gitlab_access_token]}@")
).execute ).execute
project
end end
end end
end end
......
module Gitlab
module ImportExport
class FileImporter
include Gitlab::ImportExport::CommandLineUtil
def self.import(*args)
new(*args).import
end
def initialize(archive_file:, shared:)
@archive_file = archive_file
@shared = shared
end
def import
FileUtils.mkdir_p(@shared.export_path)
decompress_archive
rescue => e
@shared.error(e)
false
end
private
def decompress_archive
untar_zxf(archive: @archive_file, dir: @shared.export_path)
end
end
end
end
...@@ -30,8 +30,6 @@ project_tree: ...@@ -30,8 +30,6 @@ project_tree:
# Only include the following attributes for the models specified. # Only include the following attributes for the models specified.
included_attributes: included_attributes:
project: project:
- :name
- :path
- :description - :description
- :issues_enabled - :issues_enabled
- :merge_requests_enabled - :merge_requests_enabled
......
module Gitlab
module ImportExport
class ImportService
def self.execute(*args)
new(*args).execute
end
def initialize(archive_file:, owner:, namespace_id:, project_path:)
@archive_file = archive_file
@current_user = owner
@namespace = Namespace.find(namespace_id)
@shared = Gitlab::ImportExport::Shared.new(relative_path: path_with_namespace(project_path), project_path: project_path)
end
def execute
Gitlab::ImportExport::Importer.import(archive_file: @archive_file,
shared: @shared)
if check_version! && [project_tree, repo_restorer, wiki_restorer, uploads_restorer].all?(&:restore)
project_tree.project
else
project_tree.project.destroy if project_tree.project
nil
end
end
private
def check_version!
Gitlab::ImportExport::VersionChecker.check!(shared: @shared)
end
def project_tree
@project_tree ||= Gitlab::ImportExport::ProjectTreeRestorer.new(user: @current_user,
shared: @shared,
namespace_id: @namespace.id)
end
def repo_restorer
Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: repo_path,
shared: @shared,
project: project_tree.project)
end
def wiki_restorer
Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: wiki_repo_path,
shared: @shared,
project: ProjectWiki.new(project_tree.project),
wiki: true)
end
def uploads_restorer
Gitlab::ImportExport::UploadsRestorer.new(project: project_tree.project, shared: @shared)
end
def path_with_namespace(project_path)
File.join(@namespace.path, project_path)
end
def repo_path
File.join(@shared.export_path, 'project.bundle')
end
def wiki_repo_path
File.join(@shared.export_path, 'project.wiki.bundle')
end
end
end
end
module Gitlab module Gitlab
module ImportExport module ImportExport
class Importer class Importer
include Gitlab::ImportExport::CommandLineUtil
def self.import(*args) def initialize(project)
new(*args).import @archive_file = project.import_source
@current_user = project.creator
@project = project
@shared = Gitlab::ImportExport::Shared.new(relative_path: path_with_namespace)
end end
def initialize(archive_file:, shared:) def execute
@archive_file = archive_file Gitlab::ImportExport::FileImporter.import(archive_file: @archive_file,
@shared = shared shared: @shared)
if check_version! && [project_tree, repo_restorer, wiki_restorer, uploads_restorer].all?(&:restore)
project_tree.restored_project
else
raise Projects::ImportService::Error.new(@shared.errors.join(', '))
end end
def import
FileUtils.mkdir_p(@shared.export_path)
decompress_archive
rescue => e
@shared.error(e)
false
end end
private private
def decompress_archive def check_version!
untar_zxf(archive: @archive_file, dir: @shared.export_path) Gitlab::ImportExport::VersionChecker.check!(shared: @shared)
end
def project_tree
@project_tree ||= Gitlab::ImportExport::ProjectTreeRestorer.new(user: @current_user,
shared: @shared,
project: @project)
end
def repo_restorer
Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: repo_path,
shared: @shared,
project: project_tree.restored_project)
end
def wiki_restorer
Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: wiki_repo_path,
shared: @shared,
project: ProjectWiki.new(project_tree.restored_project),
wiki: true)
end
def uploads_restorer
Gitlab::ImportExport::UploadsRestorer.new(project: project_tree.restored_project, shared: @shared)
end
def path_with_namespace
File.join(@project.namespace.path, @project.path)
end
def repo_path
File.join(@shared.export_path, 'project.bundle')
end
def wiki_repo_path
File.join(@shared.export_path, 'project.wiki.bundle')
end end
end end
end end
......
module Gitlab
module ImportExport
class ProjectCreator
def initialize(namespace_id, current_user, file, project_path)
@namespace_id = namespace_id
@current_user = current_user
@file = file
@project_path = project_path
end
def execute
::Projects::CreateService.new(
@current_user,
name: @project_path,
path: @project_path,
namespace_id: @namespace_id,
import_type: "gitlab_project",
import_source: @file
).execute
end
end
end
end
module Gitlab
module ImportExport
module ProjectFactory
extend self
def create(project_params:, user:, namespace_id:)
project = Project.new(project_params.except('id'))
project.creator = user
check_namespace(namespace_id, project, user)
end
def check_namespace(namespace_id, project, user)
if namespace_id
# Find matching namespace and check if it allowed
# for current user if namespace_id passed.
if allowed_namespace?(user, namespace_id)
project.namespace_id = namespace_id
else
project.namespace_id = nil
deny_namespace(project)
end
else
# Set current user namespace if namespace_id is nil
project.namespace_id = user.namespace_id
end
project
end
private
def allowed_namespace?(user, namespace_id)
namespace = Namespace.find_by(id: namespace_id)
user.can?(:create_projects, namespace)
end
def deny_namespace(project)
project.errors.add(:namespace, "is not valid")
end
end
end
end
...@@ -2,12 +2,11 @@ module Gitlab ...@@ -2,12 +2,11 @@ module Gitlab
module ImportExport module ImportExport
class ProjectTreeRestorer class ProjectTreeRestorer
def initialize(user:, shared:, namespace_id:) def initialize(user:, shared:, project:)
@path = File.join(shared.export_path, 'project.json') @path = File.join(shared.export_path, 'project.json')
@user = user @user = user
@project_path = shared.opts[:project_path]
@namespace_id = namespace_id
@shared = shared @shared = shared
@project = project
end end
def restore def restore
...@@ -20,8 +19,8 @@ module Gitlab ...@@ -20,8 +19,8 @@ module Gitlab
false false
end end
def project def restored_project
@project ||= create_project @restored_project ||= restore_project
end end
private private
...@@ -29,7 +28,7 @@ module Gitlab ...@@ -29,7 +28,7 @@ module Gitlab
def members_mapper def members_mapper
@members_mapper ||= Gitlab::ImportExport::MembersMapper.new(exported_members: @project_members, @members_mapper ||= Gitlab::ImportExport::MembersMapper.new(exported_members: @project_members,
user: @user, user: @user,
project: project) project: restored_project)
end end
# Loops through the tree of models defined in import_export.yml and # Loops through the tree of models defined in import_export.yml and
...@@ -46,7 +45,7 @@ module Gitlab ...@@ -46,7 +45,7 @@ module Gitlab
relation_key = relation.is_a?(Hash) ? relation.keys.first : relation relation_key = relation.is_a?(Hash) ? relation.keys.first : relation
relation_hash = create_relation(relation_key, @tree_hash[relation_key.to_s]) relation_hash = create_relation(relation_key, @tree_hash[relation_key.to_s])
saved << project.update_attribute(relation_key, relation_hash) saved << restored_project.update_attribute(relation_key, relation_hash)
end end
saved.all? saved.all?
end end
...@@ -57,14 +56,12 @@ module Gitlab ...@@ -57,14 +56,12 @@ module Gitlab
end end
end end
def create_project def restore_project
return @project unless @tree_hash
project_params = @tree_hash.reject { |_key, value| value.is_a?(Array) } project_params = @tree_hash.reject { |_key, value| value.is_a?(Array) }
project = Gitlab::ImportExport::ProjectFactory.create( @project.update(project_params)
project_params: project_params, user: @user, namespace_id: @namespace_id) @project
project.path = @project_path
project.name = @project_path
project.save!
project
end end
# Given a relation hash containing one or more models and its relationships, # Given a relation hash containing one or more models and its relationships,
...@@ -96,7 +93,7 @@ module Gitlab ...@@ -96,7 +93,7 @@ module Gitlab
def create_relation(relation, relation_hash_list) def create_relation(relation, relation_hash_list)
relation_array = [relation_hash_list].flatten.map do |relation_hash| relation_array = [relation_hash_list].flatten.map do |relation_hash|
Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym, Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym,
relation_hash: relation_hash.merge('project_id' => project.id), relation_hash: relation_hash.merge('project_id' => restored_project.id),
members_mapper: members_mapper, members_mapper: members_mapper,
user: @user) user: @user)
end end
......
...@@ -57,10 +57,10 @@ module Gitlab ...@@ -57,10 +57,10 @@ module Gitlab
author = @relation_hash.delete('author') author = @relation_hash.delete('author')
update_note_for_missing_author(author['name']) if missing_author? update_note_for_missing_author(author['name']) if missing_author?(old_author_id)
end end
def missing_author? def missing_author?(old_author_id)
!admin_user? || @members_mapper.missing_author_ids.include?(old_author_id) !admin_user? || @members_mapper.missing_author_ids.include?(old_author_id)
end end
......
...@@ -11,7 +11,7 @@ module Gitlab ...@@ -11,7 +11,7 @@ module Gitlab
end end
def restore def restore
return false unless File.exist?(@path_to_bundle) || wiki? return wiki? unless File.exist?(@path_to_bundle)
FileUtils.mkdir_p(path_to_repo) FileUtils.mkdir_p(path_to_repo)
......
...@@ -2,8 +2,8 @@ module Gitlab ...@@ -2,8 +2,8 @@ module Gitlab
module ImportExport module ImportExport
class VersionChecker class VersionChecker
def self.restore(*args) def self.check!(*args)
new(*args).check new(*args).check!
end end
def initialize(shared:) def initialize(shared:)
......
...@@ -20,7 +20,8 @@ module Gitlab ...@@ -20,7 +20,8 @@ module Gitlab
'Gitorious.org' => 'gitorious', 'Gitorious.org' => 'gitorious',
'Google Code' => 'google_code', 'Google Code' => 'google_code',
'FogBugz' => 'fogbugz', 'FogBugz' => 'fogbugz',
'Any repo by URL' => 'git', 'Repo by URL' => 'git',
'GitLab export' => 'gitlab_project'
} }
end end
......
require 'spec_helper'
feature 'project import', feature: true, js: true do
include Select2Helper
let(:user) { create(:admin) }
let!(:namespace) { create(:namespace, name: "asd", owner: user) }
let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') }
let(:export_path) { "#{Dir::tmpdir}/import_file_spec" }
let(:project) { Project.last }
background do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
login_as(user)
end
after(:each) do
FileUtils.rm_rf(export_path, secure: true)
end
scenario 'user imports an exported project successfully' do
expect(Project.all.count).to be_zero
visit new_project_path
select2('2', from: '#project_namespace_id')
fill_in :project_path, with:'test-project-path', visible: true
click_link 'GitLab export'
expect(page).to have_content('GitLab project export')
expect(URI.parse(current_url).query).to eq('namespace_id=2&path=test-project-path')
attach_file('file', file)
click_on 'Import project' # import starts
expect(project).not_to be_nil
expect(project.issues).not_to be_empty
expect(project.merge_requests).not_to be_empty
expect(project.repo_exists?).to be true
expect(wiki_exists?).to be true
expect(project.import_status).to eq('finished')
end
def wiki_exists?
wiki = ProjectWiki.new(project)
File.exist?(wiki.repository.path_to_repo) && !wiki.repository.empty?
end
end
...@@ -6,7 +6,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do ...@@ -6,7 +6,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:namespace) { create(:namespace, owner: user) } let(:namespace) { create(:namespace, owner: user) }
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') } let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') }
let(:project_tree_restorer) { described_class.new(user: user, shared: shared, namespace_id: namespace.id) } let(:project) { create(:empty_project, name: 'project', path: 'project') }
let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) }
let(:restored_project_json) { project_tree_restorer.restore } let(:restored_project_json) { project_tree_restorer.restore }
before do before do
......
...@@ -30,7 +30,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do ...@@ -30,7 +30,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
end end
it 'saves the correct json' do it 'saves the correct json' do
expect(saved_project_json).to include({ "name" => project.name }) expect(saved_project_json).to include({ "visibility_level" => 20 })
end end
it 'has events' do it 'has events' do
...@@ -94,7 +94,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do ...@@ -94,7 +94,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
end end
it 'has pipeline builds' do it 'has pipeline builds' do
expect(saved_project_json['pipelines'].first['statuses'].first['type']).to eq('Ci::Build') expect(saved_project_json['pipelines'].first['statuses'].count { |hash| hash['type'] == 'Ci::Build'}).to eq(1)
end end
it 'has pipeline commits' do it 'has pipeline commits' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment