Commit 2f83794d authored by Douwe Maan's avatar Douwe Maan

Merge branch 'dz-manifest-import' into 'master'

Add manifest import

See merge request gitlab-org/gitlab-ce!20304
parents 94627fd7 751e1786
...@@ -30,7 +30,13 @@ class ApplicationController < ActionController::Base ...@@ -30,7 +30,13 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception, prepend: true protect_from_forgery with: :exception, prepend: true
helper_method :can? helper_method :can?
helper_method :import_sources_enabled?, :github_import_enabled?, :gitea_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled? helper_method :import_sources_enabled?, :github_import_enabled?,
:gitea_import_enabled?, :github_import_configured?,
:gitlab_import_enabled?, :gitlab_import_configured?,
:bitbucket_import_enabled?, :bitbucket_import_configured?,
:google_code_import_enabled?, :fogbugz_import_enabled?,
:git_import_enabled?, :gitlab_project_import_enabled?,
:manifest_import_enabled?
rescue_from Encoding::CompatibilityError do |exception| rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception) log_exception(exception)
...@@ -351,6 +357,10 @@ class ApplicationController < ActionController::Base ...@@ -351,6 +357,10 @@ class ApplicationController < ActionController::Base
Gitlab::CurrentSettings.import_sources.include?('gitlab_project') Gitlab::CurrentSettings.import_sources.include?('gitlab_project')
end end
def manifest_import_enabled?
Group.supports_nested_groups? && Gitlab::CurrentSettings.import_sources.include?('manifest')
end
# U2F (universal 2nd factor) devices need a unique identifier for the application # U2F (universal 2nd factor) devices need a unique identifier for the application
# to perform authentication. # to perform authentication.
# https://developers.yubico.com/U2F/App_ID.html # https://developers.yubico.com/U2F/App_ID.html
......
class Import::ManifestController < Import::BaseController
before_action :whitelist_query_limiting, only: [:create]
before_action :verify_import_enabled
before_action :ensure_import_vars, only: [:create, :status]
def new
end
def status
@already_added_projects = find_already_added_projects
already_added_import_urls = @already_added_projects.pluck(:import_url)
@pending_repositories = repositories.to_a.reject do |repository|
already_added_import_urls.include?(repository[:url])
end
end
def upload
group = Group.find(params[:group_id])
unless can?(current_user, :create_projects, group)
@errors = ["You don't have enough permissions to create projects in the selected group"]
render :new && return
end
manifest = Gitlab::ManifestImport::Manifest.new(params[:manifest].tempfile)
if manifest.valid?
session[:manifest_import_repositories] = manifest.projects
session[:manifest_import_group_id] = group.id
redirect_to status_import_manifest_path
else
@errors = manifest.errors
render :new
end
end
def jobs
render json: find_jobs
end
def create
repository = repositories.find do |project|
project[:id] == params[:repo_id].to_i
end
project = Gitlab::ManifestImport::ProjectCreator.new(repository, group, current_user).execute
if project.persisted?
render json: ProjectSerializer.new.represent(project)
else
render json: { errors: project_save_error(project) }, status: :unprocessable_entity
end
end
private
def ensure_import_vars
unless group && repositories.present?
redirect_to(new_import_manifest_path)
end
end
def group
@group ||= Group.find_by(id: session[:manifest_import_group_id])
end
def repositories
@repositories ||= session[:manifest_import_repositories]
end
def find_jobs
find_already_added_projects.to_json(only: [:id], methods: [:import_status])
end
def find_already_added_projects
group.all_projects
.where(import_type: 'manifest')
.where(creator_id: current_user)
.includes(:import_state)
end
def verify_import_enabled
render_404 unless manifest_import_enabled?
end
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/48939')
end
end
...@@ -3,7 +3,7 @@ module NamespacesHelper ...@@ -3,7 +3,7 @@ module NamespacesHelper
params.dig(:project, :namespace_id) || params[:namespace_id] params.dig(:project, :namespace_id) || params[:namespace_id]
end end
def namespaces_options(selected = :current_user, display_path: false, extra_group: nil) def namespaces_options(selected = :current_user, display_path: false, extra_group: nil, groups_only: false)
groups = current_user.manageable_groups groups = current_user.manageable_groups
.joins(:route) .joins(:route)
.includes(:route) .includes(:route)
...@@ -20,11 +20,14 @@ module NamespacesHelper ...@@ -20,11 +20,14 @@ module NamespacesHelper
options = [] options = []
options << options_for_group(groups, display_path: display_path, type: 'group') options << options_for_group(groups, display_path: display_path, type: 'group')
unless groups_only
options << options_for_group(users, display_path: display_path, type: 'user') options << options_for_group(users, display_path: display_path, type: 'user')
if selected == :current_user && current_user.namespace if selected == :current_user && current_user.namespace
selected = current_user.namespace.id selected = current_user.namespace.id
end end
end
grouped_options_for_select(options, selected) grouped_options_for_select(options, selected)
end end
......
module Groups module Groups
class NestedCreateService < Groups::BaseService class NestedCreateService < Groups::BaseService
attr_reader :group_path attr_reader :group_path, :visibility_level
def initialize(user, params) def initialize(user, params)
@current_user, @params = user, params.dup @current_user, @params = user, params.dup
@group_path = @params.delete(:group_path) @group_path = @params.delete(:group_path)
@visibility_level = @params.delete(:visibility_level) ||
Gitlab::CurrentSettings.current_application_settings.default_group_visibility
end end
def execute def execute
...@@ -36,11 +37,12 @@ module Groups ...@@ -36,11 +37,12 @@ module Groups
new_params = params.reverse_merge( new_params = params.reverse_merge(
path: subgroup_name, path: subgroup_name,
name: subgroup_name, name: subgroup_name,
parent: last_group parent: last_group,
visibility_level: visibility_level
) )
new_params[:visibility_level] ||= Gitlab::CurrentSettings.current_application_settings.default_group_visibility
last_group = namespace_or_group(partial_path) || Groups::CreateService.new(current_user, new_params).execute last_group = namespace_or_group(partial_path) ||
Groups::CreateService.new(current_user, new_params).execute
end end
last_group last_group
......
...@@ -21,23 +21,13 @@ ...@@ -21,23 +21,13 @@
%th= _('Status') %th= _('Status')
%tbody %tbody
- @already_added_projects.each do |project| - @already_added_projects.each do |project|
%tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } %tr{ id: "project_#{project.id}", class: project_status_css_class(project.import_status) }
%td %td
= provider_project_link(provider, project.import_source) = provider_project_link(provider, project.import_source)
%td %td
= link_to project.full_path, [project.namespace.becomes(Namespace), project] = link_to project.full_path, [project.namespace.becomes(Namespace), project]
%td.job-status %td.job-status
- if project.import_status == 'finished' = render 'import/project_status', project: project
%span
%i.fa.fa-check
= _('Done')
- elsif project.import_status == 'started'
%i.fa.fa-spinner.fa-spin
= _('Started')
- elsif project.import_status == 'failed'
= _('Failed')
- else
= project.human_import_status_name
- @repos.each do |repo| - @repos.each do |repo|
%tr{ id: "repo_#{repo.id}", data: { qa: { repo_path: repo.full_name } } } %tr{ id: "repo_#{repo.id}", data: { qa: { repo_path: repo.full_name } } }
...@@ -61,6 +51,6 @@ ...@@ -61,6 +51,6 @@
= has_ci_cd_only_params? ? _('Connect') : _('Import') = has_ci_cd_only_params? ? _('Connect') : _('Import')
= icon("spinner spin", class: "loading-icon") = icon("spinner spin", class: "loading-icon")
.js-importer-status{ data: { jobs_import_path: "#{url_for([:jobs, :import, provider])}", .js-importer-status{ data: { jobs_import_path: url_for([:jobs, :import, provider]),
import_path: "#{url_for([:import, provider])}", import_path: url_for([:import, provider]),
ci_cd_only: "#{has_ci_cd_only_params?}" } } ci_cd_only: has_ci_cd_only_params?.to_s } }
- case project.import_status
- when 'finished'
= icon('check')
= _('Done')
- when 'started'
= icon("spinner spin")
= _('Started')
- when 'failed'
= _('Failed')
- else
= project.human_import_status_name
= form_tag upload_import_manifest_path, multipart: true do
.form-group
= label_tag :group_id, nil, class: 'label-light' do
= _('Group')
.input-group
.input-group-prepend.has-tooltip{ title: root_url }
.input-group-text
= root_url
= select_tag :group_id, namespaces_options(nil, display_path: true, groups_only: true), { class: 'select2 js-select-namespace' }
.form-text.text-muted
= _('Choose the top-level group for your repository imports.')
.form-group
= label_tag :manifest, class: 'label-light' do
= _('Manifest')
= file_field_tag :manifest, class: 'form-control-file', required: true
.form-text.text-muted
= _('Import multiple repositories by uploading a manifest file.')
= link_to icon('question-circle'), help_page_path('user/project/import/manifest')
.append-bottom-10
= submit_tag _('List available repositories'), class: 'btn btn-success'
= link_to _('Cancel'), new_project_path, class: 'btn btn-cancel'
- page_title "Manifest file import"
- header_title "Projects", root_path
%h3.page-title
= _('Manifest file import')
- if @errors.present?
.alert.alert-danger
- @errors.each do |error|
= error
= render 'form'
- page_title "Manifest import"
- header_title "Projects", root_path
- provider = 'manifest'
%h3.page-title
= _('Manifest file import')
%p
= button_tag class: "btn btn-import btn-success js-import-all" do
= import_all_githubish_repositories_button_label
= icon("spinner spin", class: "loading-icon")
.table-responsive
%table.table.import-jobs
%thead
%tr
%th= _('Repository URL')
%th= _('To GitLab')
%th= _('Status')
%tbody
- @already_added_projects.each do |project|
%tr{ id: "project_#{project.id}", class: project_status_css_class(project.import_status) }
%td
= project.import_url
%td
= link_to_project project
%td.job-status
= render 'import/project_status', project: project
- @pending_repositories.each do |repository|
%tr{ id: "repo_#{repository[:id]}" }
%td
= repository[:url]
%td.import-target
= import_project_target(@group.full_path, repository[:path])
%td.import-actions.job-status
= button_tag class: "btn btn-import js-add-to-import" do
= _('Import')
= icon("spinner spin", class: "loading-icon")
.js-importer-status{ data: { jobs_import_path: url_for([:jobs, :import, provider]),
import_path: url_for([:import, provider]) } }
- active_tab = local_assigns.fetch(:active_tab, 'blank') - active_tab = local_assigns.fetch(:active_tab, 'blank')
- f = local_assigns.fetch(:f)
.project-import .project-import
.form-group.import-btn-container.clearfix .form-group.import-btn-container.clearfix
= f.label :visibility_level, class: 'label-light' do #the label here seems wrong %h5
Import project from Import project from
.import-buttons .import-buttons
- if gitlab_project_import_enabled? - if gitlab_project_import_enabled?
.import_gitlab_project.has-tooltip{ data: { container: 'body' } } .import_gitlab_project.has-tooltip{ data: { container: 'body' } }
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
= icon('gitlab', text: 'GitLab export') = icon('gitlab', text: 'GitLab export')
%div
- if github_import_enabled? - if github_import_enabled?
%div
= link_to new_import_github_path, class: 'btn js-import-github' do = link_to new_import_github_path, class: 'btn js-import-github' do
= icon('github', text: 'GitHub') = icon('github', text: 'GitHub')
%div
- if bitbucket_import_enabled? - if bitbucket_import_enabled?
%div
= link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do
= icon('bitbucket', text: 'Bitbucket') = icon('bitbucket', text: 'Bitbucket')
- unless bitbucket_import_configured? - unless bitbucket_import_configured?
= render 'bitbucket_import_modal' = render 'bitbucket_import_modal'
%div
- if gitlab_import_enabled? - if gitlab_import_enabled?
%div
= link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do
= icon('gitlab', text: 'GitLab.com') = icon('gitlab', text: 'GitLab.com')
- unless gitlab_import_configured? - unless gitlab_import_configured?
= render 'gitlab_import_modal' = render 'gitlab_import_modal'
%div
- if google_code_import_enabled? - if google_code_import_enabled?
%div
= link_to new_import_google_code_path, class: 'btn import_google_code' do = link_to new_import_google_code_path, class: 'btn import_google_code' do
= icon('google', text: 'Google Code') = icon('google', text: 'Google Code')
%div
- if fogbugz_import_enabled? - if fogbugz_import_enabled?
%div
= link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
= icon('bug', text: 'Fogbugz') = icon('bug', text: 'Fogbugz')
%div
- if gitea_import_enabled? - if gitea_import_enabled?
%div
= link_to new_import_gitea_path, class: 'btn import_gitea' do = link_to new_import_gitea_path, class: 'btn import_gitea' do
= custom_icon('go_logo') = custom_icon('go_logo')
Gitea Gitea
%div
- if git_import_enabled? - if git_import_enabled?
%div
%button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } } %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } }
= icon('git', text: 'Repo by URL') = icon('git', text: 'Repo by URL')
- if manifest_import_enabled?
%div
= link_to new_import_manifest_path, class: 'btn import_manifest' do
= icon('file-text-o', text: 'Manifest file')
.js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') } .js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') }
= form_for @project, html: { class: 'new_project' } do |f|
%hr %hr
= render "shared/import_form", f: f = render "shared/import_form", f: f
= render 'new_project_fields', f: f, project_name_id: "import-url-name" = render 'new_project_fields', f: f, project_name_id: "import-url-name"
...@@ -55,9 +55,8 @@ ...@@ -55,9 +55,8 @@
= render 'project_templates', f: f = render 'project_templates', f: f
.tab-pane.import-project-pane.js-toggle-container{ id: 'import-project-pane', class: active_when(active_tab == 'import'), role: 'tabpanel' } .tab-pane.import-project-pane.js-toggle-container{ id: 'import-project-pane', class: active_when(active_tab == 'import'), role: 'tabpanel' }
= form_for @project, html: { class: 'new_project' } do |f|
- if import_sources_enabled? - if import_sources_enabled?
= render 'import_project_pane', f: f, active_tab: active_tab = render 'import_project_pane', active_tab: active_tab
- else - else
.nothing-here-block .nothing-here-block
%h4 No import options available %h4 No import options available
......
---
title: Add ability to import multiple repositories by uploading a manifest file
merge_request: 20304
author:
type: added
...@@ -45,4 +45,10 @@ namespace :import do ...@@ -45,4 +45,10 @@ namespace :import do
resource :gitlab_project, only: [:create, :new] do resource :gitlab_project, only: [:create, :new] do
post :create post :create
end end
resource :manifest, only: [:create, :new], controller: :manifest do
get :status
get :jobs
post :upload
end
end end
...@@ -105,7 +105,7 @@ PUT /application/settings ...@@ -105,7 +105,7 @@ PUT /application/settings
| `housekeeping_gc_period` | integer | no | Number of Git pushes after which 'git gc' is run. | | `housekeeping_gc_period` | integer | no | Number of Git pushes after which 'git gc' is run. |
| `housekeeping_incremental_repack_period` | integer | no | Number of Git pushes after which an incremental 'git repack' is run. | | `housekeeping_incremental_repack_period` | integer | no | Number of Git pushes after which an incremental 'git repack' is run. |
| `html_emails_enabled` | boolean | no | Enable HTML emails | | `html_emails_enabled` | boolean | no | Enable HTML emails |
| `import_sources` | Array of strings | no | Sources to allow project import from, possible values: "github bitbucket gitlab google_code fogbugz git gitlab_project | | `import_sources` | Array of strings | no | Sources to allow project import from, possible values: "github bitbucket gitlab google_code fogbugz git gitlab_project manifest |
| `koding_enabled` | boolean | no | Enable Koding integration. Default is `false`. | | `koding_enabled` | boolean | no | Enable Koding integration. Default is `false`. |
| `koding_url` | string | yes (if `koding_enabled` is `true`) | The Koding instance URL for integration. | | `koding_url` | string | yes (if `koding_enabled` is `true`) | The Koding instance URL for integration. |
| `max_artifacts_size` | integer | no | Maximum artifacts size in MB | | `max_artifacts_size` | integer | no | Maximum artifacts size in MB |
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
1. [From SVN](svn.md) 1. [From SVN](svn.md)
1. [From TFS](tfs.md) 1. [From TFS](tfs.md)
1. [From repo by URL](repo_by_url.md) 1. [From repo by URL](repo_by_url.md)
1. [By uploading a manifest file](manifest.md)
In addition to the specific migration documentation above, you can import any In addition to the specific migration documentation above, you can import any
Git repository via HTTP from the New Project page. Be aware that if the Git repository via HTTP from the New Project page. Be aware that if the
......
# Import multiple repositories by uploading a manifest file
GitLab allows you to import all the required git repositories
based a manifest file like the one used by the Android repository.
>**Note:**
This feature requires [subgroups](../../group/subgroups/index.md) to be supported by your database.
You can do it by following next steps:
1. From your GitLab dashboard click **New project**
1. Switch to the **Import project** tab
1. Click on the **Manifest file** button
1. Provide GitLab with a manifest xml file
1. Select a group you want to import to (you need to create a group first if you don't have one)
1. Click **List available repositories**
1. You will be redirected to the import status page with projects list based on manifest file
1. Check the list and click 'Import all repositories' to start import.
![Manifest upload](img/manifest_upload.png)
![Manifest status](img/manifest_status.png)
### Manifest format
A manifest must be an XML file. There must be one `remote` tag with `review` attribute
that contains a URL to a git server. Each `project` tag must have `name` and `path` attribute.
GitLab will build URL to the repository by combining URL from `remote` tag with a project name.
A path attribute will be used to represent project path in GitLab system.
Below is a valid example of manifest file.
```xml
<manifest>
<remote review="https://android-review.googlesource.com/" />
<project path="build/make" name="platform/build" />
<project path="build/blueprint" name="platform/build/blueprint" />
</manifest>
```
As result next projects will be created:
| GitLab | Import URL |
|---|---|
| https://gitlab/YOUR_GROUP/build/make | https://android-review.googlesource.com/platform/build |
| https://gitlab/YOUR_GROUP/build/blueprint | https://android-review.googlesource.com/platform/build/blueprint |
...@@ -25,7 +25,7 @@ module API ...@@ -25,7 +25,7 @@ module API
optional :default_snippet_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default snippet visibility' optional :default_snippet_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default snippet visibility'
optional :default_group_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default group visibility' optional :default_group_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default group visibility'
optional :restricted_visibility_levels, type: Array[String], desc: 'Selected levels cannot be used by non-admin users for groups, projects or snippets. If the public level is restricted, user profiles are only visible to logged in users.' optional :restricted_visibility_levels, type: Array[String], desc: 'Selected levels cannot be used by non-admin users for groups, projects or snippets. If the public level is restricted, user profiles are only visible to logged in users.'
optional :import_sources, type: Array[String], values: %w[github bitbucket gitlab google_code fogbugz git gitlab_project], optional :import_sources, type: Array[String], values: %w[github bitbucket gitlab google_code fogbugz git gitlab_project manifest],
desc: 'Enabled sources for code import during project creation. OmniAuth must be configured for GitHub, Bitbucket, and GitLab.com' desc: 'Enabled sources for code import during project creation. OmniAuth must be configured for GitHub, Bitbucket, and GitLab.com'
optional :disabled_oauth_sign_in_sources, type: Array[String], desc: 'Disable certain OAuth sign-in sources' optional :disabled_oauth_sign_in_sources, type: Array[String], desc: 'Disable certain OAuth sign-in sources'
optional :enabled_git_access_protocol, type: String, values: %w[ssh http nil], desc: 'Allow only the selected protocols to be used for Git access.' optional :enabled_git_access_protocol, type: String, values: %w[ssh http nil], desc: 'Allow only the selected protocols to be used for Git access.'
......
...@@ -16,7 +16,8 @@ module Gitlab ...@@ -16,7 +16,8 @@ module Gitlab
ImportSource.new('fogbugz', 'FogBugz', Gitlab::FogbugzImport::Importer), ImportSource.new('fogbugz', 'FogBugz', Gitlab::FogbugzImport::Importer),
ImportSource.new('git', 'Repo by URL', nil), ImportSource.new('git', 'Repo by URL', nil),
ImportSource.new('gitlab_project', 'GitLab export', Gitlab::ImportExport::Importer), ImportSource.new('gitlab_project', 'GitLab export', Gitlab::ImportExport::Importer),
ImportSource.new('gitea', 'Gitea', Gitlab::LegacyGithubImport::Importer) ImportSource.new('gitea', 'Gitea', Gitlab::LegacyGithubImport::Importer),
ImportSource.new('manifest', 'Manifest file', nil)
].freeze ].freeze
class << self class << self
......
# Class to parse manifest file and build a list of repositories for import
#
# <manifest>
# <remote review="https://android-review.googlesource.com/" />
# <project path="platform-common" name="platform" />
# <project path="platform/art" name="platform/art" />
# <project path="platform/device" name="platform/device" />
# </manifest>
#
# 1. Project path must be uniq and can't be part of other project path.
# For example, you can't have projects with 'foo' and 'foo/bar' paths.
# 2. Remote must be present with review attribute so GitLab knows
# where to fetch source code
module Gitlab
module ManifestImport
class Manifest
attr_reader :parsed_xml, :errors
def initialize(file)
@parsed_xml = Nokogiri::XML(file) { |config| config.strict }
@errors = []
rescue Nokogiri::XML::SyntaxError
@errors = ['The uploaded file is not a valid XML file.']
end
def projects
raw_projects.each_with_index.map do |project, i|
{
id: i,
name: project['name'],
path: project['path'],
url: repository_url(project['name'])
}
end
end
def valid?
return false if @errors.any?
unless validate_remote
@errors << 'Make sure a <remote> tag is present and is valid.'
end
unless validate_projects
@errors << 'Make sure every <project> tag has name and path attributes.'
end
@errors.empty?
end
private
def validate_remote
remote.present? && URI.parse(remote).host
rescue URI::Error
false
end
def validate_projects
raw_projects.all? do |project|
project['name'] && project['path']
end
end
def repository_url(name)
URI.join(remote, name).to_s
end
def remote
return @remote if defined?(@remote)
remote_tag = parsed_xml.css('manifest > remote').first
@remote = remote_tag['review'] if remote_tag
end
def raw_projects
@raw_projects ||= parsed_xml.css('manifest > project')
end
end
end
end
module Gitlab
module ManifestImport
class ProjectCreator
attr_reader :repository, :destination, :current_user
def initialize(repository, destination, current_user)
@repository = repository
@destination = destination
@current_user = current_user
end
def execute
group_full_path, _, project_path = repository[:path].rpartition('/')
group_full_path = File.join(destination.full_path, group_full_path) if destination
group = create_group_with_parents(group_full_path)
params = {
import_url: repository[:url],
import_type: 'manifest',
namespace_id: group.id,
path: project_path,
name: project_path,
visibility_level: destination.visibility_level
}
Projects::CreateService.new(current_user, params).execute
end
private
def create_group_with_parents(full_path)
params = {
group_path: full_path,
visibility_level: destination.visibility_level
}
Groups::NestedCreateService.new(current_user, params).execute
end
end
end
end
...@@ -981,6 +981,9 @@ msgstr "" ...@@ -981,6 +981,9 @@ msgstr ""
msgid "Choose file..." msgid "Choose file..."
msgstr "" msgstr ""
msgid "Choose the top-level group for your repository imports."
msgstr ""
msgid "Choose which repositories you want to import." msgid "Choose which repositories you want to import."
msgstr "" msgstr ""
...@@ -2447,6 +2450,9 @@ msgstr "" ...@@ -2447,6 +2450,9 @@ msgstr ""
msgid "Graph" msgid "Graph"
msgstr "" msgstr ""
msgid "Group"
msgstr ""
msgid "Group CI/CD settings" msgid "Group CI/CD settings"
msgstr "" msgstr ""
...@@ -2650,6 +2656,9 @@ msgstr "" ...@@ -2650,6 +2656,9 @@ msgstr ""
msgid "Import in progress" msgid "Import in progress"
msgstr "" msgstr ""
msgid "Import multiple repositories by uploading a manifest file."
msgstr ""
msgid "Import repositories from GitHub" msgid "Import repositories from GitHub"
msgstr "" msgstr ""
...@@ -2859,6 +2868,9 @@ msgstr "" ...@@ -2859,6 +2868,9 @@ msgstr ""
msgid "List" msgid "List"
msgstr "" msgstr ""
msgid "List available repositories"
msgstr ""
msgid "List your GitHub repositories" msgid "List your GitHub repositories"
msgstr "" msgstr ""
...@@ -2898,6 +2910,12 @@ msgstr "" ...@@ -2898,6 +2910,12 @@ msgstr ""
msgid "Manage project labels" msgid "Manage project labels"
msgstr "" msgstr ""
msgid "Manifest"
msgstr ""
msgid "Manifest file import"
msgstr ""
msgid "Mar" msgid "Mar"
msgstr "" msgstr ""
...@@ -3920,6 +3938,9 @@ msgstr "" ...@@ -3920,6 +3938,9 @@ msgstr ""
msgid "Repository Settings" msgid "Repository Settings"
msgstr "" msgstr ""
msgid "Repository URL"
msgstr ""
msgid "Repository maintenance" msgid "Repository maintenance"
msgstr "" msgstr ""
......
require 'spec_helper'
describe 'Import multiple repositories by uploading a manifest file', :js, :postgresql do
include Select2Helper
let(:user) { create(:admin) }
let(:group) { create(:group) }
before do
sign_in(user)
group.add_owner(user)
end
it 'parses manifest file and list repositories' do
visit new_import_manifest_path
attach_file('manifest', Rails.root.join('spec/fixtures/aosp_manifest.xml'))
click_on 'List available repositories'
expect(page).to have_button('Import all repositories')
expect(page).to have_content('https://android-review.googlesource.com/platform/build/blueprint')
end
it 'imports succesfully imports a project' do
visit new_import_manifest_path
attach_file('manifest', Rails.root.join('spec/fixtures/aosp_manifest.xml'))
click_on 'List available repositories'
page.within(first_row) do
click_on 'Import'
expect(page).to have_content 'Done'
expect(page).to have_content("#{group.full_path}/build/make")
end
end
it 'renders an error if invalid file was provided' do
visit new_import_manifest_path
attach_file('manifest', Rails.root.join('spec/fixtures/banana_sample.gif'))
click_on 'List available repositories'
expect(page).to have_content 'The uploaded file is not a valid XML file.'
end
def first_row
page.all('table.import-jobs tbody tr')[0]
end
end
...@@ -25,6 +25,22 @@ describe 'New project' do ...@@ -25,6 +25,22 @@ describe 'New project' do
expect(page).to have_link('GitLab export') expect(page).to have_link('GitLab export')
end end
describe 'manifest import option' do
before do
visit new_project_path
find('#import-project-tab').click
end
context 'when using postgres', :postgresql do
it { expect(page).to have_link('Manifest file') }
end
context 'when using mysql', :mysql do
it { expect(page).not_to have_link('Manifest file') }
end
end
context 'Visibility level selector', :js do context 'Visibility level selector', :js do
Gitlab::VisibilityLevel.options.each do |key, level| Gitlab::VisibilityLevel.options.each do |key, level|
it "sets selector to #{key}" do it "sets selector to #{key}" do
...@@ -201,5 +217,16 @@ describe 'New project' do ...@@ -201,5 +217,16 @@ describe 'New project' do
expect(current_path).to eq new_import_google_code_path expect(current_path).to eq new_import_google_code_path
end end
end end
context 'from manifest file', :postgresql do
before do
first('.import_manifest').click
end
it 'shows import instructions' do
expect(page).to have_content('Manifest file import')
expect(current_path).to eq new_import_manifest_path
end
end
end end
end end
This diff is collapsed.
...@@ -28,6 +28,16 @@ describe NamespacesHelper do ...@@ -28,6 +28,16 @@ describe NamespacesHelper do
expect(options).not_to include(admin_group.name) expect(options).not_to include(admin_group.name)
expect(options).to include(user_group.name) expect(options).to include(user_group.name)
expect(options).to include(user.name)
end
it 'returns only groups if groups_only option is true' do
allow(helper).to receive(:current_user).and_return(user)
options = helper.namespaces_options(nil, groups_only: true)
expect(options).not_to include(user.name)
expect(options).to include(user_group.name)
end end
context 'when nested groups are available', :nested_groups do context 'when nested groups are available', :nested_groups do
......
...@@ -12,7 +12,8 @@ describe Gitlab::ImportSources do ...@@ -12,7 +12,8 @@ describe Gitlab::ImportSources do
'FogBugz' => 'fogbugz', 'FogBugz' => 'fogbugz',
'Repo by URL' => 'git', 'Repo by URL' => 'git',
'GitLab export' => 'gitlab_project', 'GitLab export' => 'gitlab_project',
'Gitea' => 'gitea' 'Gitea' => 'gitea',
'Manifest file' => 'manifest'
} }
expect(described_class.options).to eq(expected) expect(described_class.options).to eq(expected)
...@@ -31,6 +32,7 @@ describe Gitlab::ImportSources do ...@@ -31,6 +32,7 @@ describe Gitlab::ImportSources do
git git
gitlab_project gitlab_project
gitea gitea
manifest
) )
expect(described_class.values).to eq(expected) expect(described_class.values).to eq(expected)
...@@ -63,7 +65,8 @@ describe Gitlab::ImportSources do ...@@ -63,7 +65,8 @@ describe Gitlab::ImportSources do
'fogbugz' => Gitlab::FogbugzImport::Importer, 'fogbugz' => Gitlab::FogbugzImport::Importer,
'git' => nil, 'git' => nil,
'gitlab_project' => Gitlab::ImportExport::Importer, 'gitlab_project' => Gitlab::ImportExport::Importer,
'gitea' => Gitlab::LegacyGithubImport::Importer 'gitea' => Gitlab::LegacyGithubImport::Importer,
'manifest' => nil
} }
import_sources.each do |name, klass| import_sources.each do |name, klass|
...@@ -82,7 +85,8 @@ describe Gitlab::ImportSources do ...@@ -82,7 +85,8 @@ describe Gitlab::ImportSources do
'fogbugz' => 'FogBugz', 'fogbugz' => 'FogBugz',
'git' => 'Repo by URL', 'git' => 'Repo by URL',
'gitlab_project' => 'GitLab export', 'gitlab_project' => 'GitLab export',
'gitea' => 'Gitea' 'gitea' => 'Gitea',
'manifest' => 'Manifest file'
} }
import_sources.each do |name, title| import_sources.each do |name, title|
......
require 'spec_helper'
describe Gitlab::ManifestImport::Manifest, :postgresql do
let(:file) { File.open(Rails.root.join('spec/fixtures/aosp_manifest.xml')) }
let(:manifest) { described_class.new(file) }
describe '#valid?' do
context 'valid file' do
it { expect(manifest.valid?).to be true }
end
context 'missing or invalid attributes' do
let(:file) { Tempfile.new('foo') }
before do
content = <<~EOS
<manifest>
<remote review="invalid-url" />
<project name="platform/build"/>
</manifest>
EOS
file.write(content)
file.rewind
end
it { expect(manifest.valid?).to be false }
describe 'errors' do
before do
manifest.valid?
end
it { expect(manifest.errors).to include('Make sure a <remote> tag is present and is valid.') }
it { expect(manifest.errors).to include('Make sure every <project> tag has name and path attributes.') }
end
end
end
describe '#projects' do
it { expect(manifest.projects.size).to eq(660) }
it { expect(manifest.projects[0][:name]).to eq('platform/build') }
it { expect(manifest.projects[0][:path]).to eq('build/make') }
it { expect(manifest.projects[0][:url]).to eq('https://android-review.googlesource.com/platform/build') }
end
end
require 'spec_helper'
describe Gitlab::ManifestImport::ProjectCreator, :postgresql do
let(:group) { create(:group) }
let(:user) { create(:user) }
let(:repository) do
{
path: 'device/common',
url: 'https://android-review.googlesource.com/device/common'
}
end
before do
group.add_owner(user)
end
subject { described_class.new(repository, group, user) }
describe '#execute' do
it { expect(subject.execute).to be_a(Project) }
it { expect { subject.execute }.to change { Project.count }.by(1) }
it { expect { subject.execute }.to change { Group.count }.by(1) }
it 'creates project with valid full path and import url' do
subject.execute
project = Project.last
expect(project.full_path).to eq(File.join(group.path, 'device/common'))
expect(project.import_url).to eq('https://android-review.googlesource.com/device/common')
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