Commit 1d3815f8 authored by Z.J. van de Weg's avatar Z.J. van de Weg

Allow projects to be started from a template

Started implementation for the first iteration of
gitlab-org/gitlab-ce#32420. This will allow users to select a template
to start with, instead of an empty repository in the project just
created.

Internally this is basically a small extension of the ImportExport
GitLab projects we already support. We just import a certain import
tar archive. This commits includes the first one: Ruby on Rails. In the
future more will be added.
parent d964816b
...@@ -12,15 +12,7 @@ class Import::GitlabProjectsController < Import::BaseController ...@@ -12,15 +12,7 @@ class Import::GitlabProjectsController < Import::BaseController
return redirect_back_or_default(options: { alert: "You need to upload a GitLab project export archive." }) return redirect_back_or_default(options: { alert: "You need to upload a GitLab project export archive." })
end end
import_upload_path = Gitlab::ImportExport.import_upload_path(filename: project_params[:file].original_filename) @project = ::Projects::GitlabProjectsImporterService.new(current_user, project_params).execute
FileUtils.mkdir_p(File.dirname(import_upload_path))
FileUtils.copy_entry(project_params[:file].path, import_upload_path)
@project = Gitlab::ImportExport::ProjectCreator.new(project_params[:namespace_id],
current_user,
import_upload_path,
project_params[:path]).execute
if @project.saved? if @project.saved?
redirect_to( redirect_to(
......
...@@ -27,7 +27,12 @@ class ProjectsController < Projects::ApplicationController ...@@ -27,7 +27,12 @@ class ProjectsController < Projects::ApplicationController
end end
def create def create
@project = ::Projects::CreateService.new(current_user, project_params).execute @project =
if project_from_template?
::Projects::CreateFromTemplateService.new(current_user, project_params).execute
else
::Projects::CreateService.new(current_user, project_params).execute
end
if @project.saved? if @project.saved?
cookies[:issue_board_welcome_hidden] = { path: project_path(@project), value: nil, expires: Time.at(0) } cookies[:issue_board_welcome_hidden] = { path: project_path(@project), value: nil, expires: Time.at(0) }
...@@ -324,6 +329,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -324,6 +329,7 @@ class ProjectsController < Projects::ApplicationController
:runners_token, :runners_token,
:tag_list, :tag_list,
:visibility_level, :visibility_level,
:template_title,
project_feature_attributes: %i[ project_feature_attributes: %i[
builds_access_level builds_access_level
...@@ -345,6 +351,10 @@ class ProjectsController < Projects::ApplicationController ...@@ -345,6 +351,10 @@ class ProjectsController < Projects::ApplicationController
false false
end end
def project_from_template?
project_params[:template_title]&.present?
end
def project_view_files? def project_view_files?
if current_user if current_user
current_user.project_view == 'files' current_user.project_view == 'files'
......
...@@ -71,6 +71,7 @@ class Project < ActiveRecord::Base ...@@ -71,6 +71,7 @@ class Project < ActiveRecord::Base
attr_accessor :new_default_branch attr_accessor :new_default_branch
attr_accessor :old_path_with_namespace attr_accessor :old_path_with_namespace
attr_accessor :template_title
attr_writer :pipeline_status attr_writer :pipeline_status
alias_attribute :title, :name alias_attribute :title, :name
......
module Projects
class CreateFromTemplateService < BaseService
def initialize(user, params)
@current_user, @params = user, params.dup
end
def execute
params[:file] = Gitlab::ProjectTemplate.find(params[:template_title]).file
@params[:from_template] = true
GitlabProjectsImporterService.new(@current_user, @params).execute
end
end
end
# This service is an adapter used to for the GitLab Import feature, and
# creating a project from a template.
# The latter will under the hood just import an archive supplied by GitLab.
module Projects
class GitlabProjectsImporterService
attr_reader :current_user, :params
def initialize(user, params)
@current_user, @params = user, params.dup
end
def execute
FileUtils.mkdir_p(File.dirname(import_upload_path))
FileUtils.copy_entry(file.path, import_upload_path)
Gitlab::ImportExport::ProjectCreator.new(params[:namespace_id],
current_user,
import_upload_path,
params[:path]).execute
end
private
def import_upload_path
@import_upload_path ||= Gitlab::ImportExport
.import_upload_path(filename: "#{params[:namespace_id]}-#{params[:path]}")
end
def file
params[:file]
end
end
end
.col-sm-12.template-buttons
- Gitlab::ProjectTemplate.all.each do |template|
-# The title should be the value posted to the controller, a pretty name to print would be
-# template.name
= template.title
= image_tag(template.logo_path)
= f.text_field :template_title, placeholder: "rails", class: "form-control", tabindex: 2, autofocus: true, required: true
...@@ -17,36 +17,17 @@ ...@@ -17,36 +17,17 @@
Create or Import your project from popular Git services Create or Import your project from popular Git services
.col-lg-9 .col-lg-9
= form_for @project, html: { class: 'new_project' } do |f| = form_for @project, html: { class: 'new_project' } do |f|
.row .project-template.js-toggle-container
.form-group.col-xs-12.col-sm-6 .form_group.clearfix
= f.label :namespace_id, class: 'label-light' do = f.label :template_project, class: 'label-light' do
%span Start from template
Project path .col-sm-12.import-buttons
.form-group = render 'project_templates', f: f
.input-group
- if current_user.can_select_namespace?
.input-group-addon
= root_url
= f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), {}, { class: 'select2 js-select-namespace', tabindex: 1}
- else
.input-group-addon.static-namespace
#{root_url}#{current_user.username}/
= f.hidden_field :namespace_id, value: current_user.namespace_id
.form-group.col-xs-12.col-sm-6.project-path
= f.label :path, class: 'label-light' do
%span
Project name
= f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 2, autofocus: true, required: true
- if current_user.can_create_group?
.help-block
Want to house several dependent projects under the same namespace?
= link_to "Create a group", new_group_path
- if import_sources_enabled? - if import_sources_enabled?
.project-import.js-toggle-container .project-import.js-toggle-container
.form-group.clearfix .form-group.clearfix
= f.label :visibility_level, class: 'label-light' do = f.label :visibility_level, class: 'label-light' do #the label here seems wrong
Import project from Import project from
.col-sm-12.import-buttons .col-sm-12.import-buttons
%div %div
...@@ -90,6 +71,32 @@ ...@@ -90,6 +71,32 @@
.js-toggle-content.hide .js-toggle-content.hide
= render "shared/import_form", f: f = render "shared/import_form", f: f
.row
.form-group.col-xs-12.col-sm-6
= f.label :namespace_id, class: 'label-light' do
%span
Project path
.form-group
.input-group
- if current_user.can_select_namespace?
.input-group-addon
= root_url
= f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), {}, { class: 'select2 js-select-namespace', tabindex: 1}
- else
.input-group-addon.static-namespace
#{root_url}#{current_user.username}/
= f.hidden_field :namespace_id, value: current_user.namespace_id
.form-group.col-xs-12.col-sm-6.project-path
= f.label :path, class: 'label-light' do
%span
Project name
= f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 2, autofocus: true, required: true
- if current_user.can_create_group?
.help-block
Want to house several dependent projects under the same namespace?
= link_to "Create a group", new_group_path
.form-group .form-group
= f.label :description, class: 'label-light' do = f.label :description, class: 'label-light' do
Project description Project description
......
...@@ -15,7 +15,9 @@ module Gitlab ...@@ -15,7 +15,9 @@ module Gitlab
end end
def import_upload_path(filename:) def import_upload_path(filename:)
File.join(storage_path, 'uploads', filename) milliseconds = Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond)
File.join(storage_path, 'uploads', "#{millisecond}-#{filename}")
end end
def project_filename def project_filename
......
module Gitlab
class ProjectTemplate
attr_reader :title, :name
def initialize(name, title)
@name, @title = name, title
end
def logo_path
"project_templates/#{name}.png"
end
def file
template_archive.open
end
def template_archive
Rails.root.join("vendor/project_templates/#{name}.tar.gz")
end
def ==(other)
name == other.name && title == other.title
end
TemplatesTable = [
ProjectTemplate.new('rails', 'Ruby on Rails')
].freeze
class << self
def all
TemplatesTable
end
def find(name)
all.find { |template| template.name == name.to_s }
end
end
end
end
require 'spec_helper' require 'spec_helper'
feature 'Project', feature: true do feature 'Project', feature: true do
describe 'creating from template' do
let(:user) { create(:user) }
let(:template) { Gitlab::ProjectTemplate.find(:rails) }
before do
sign_in user
visit new_project_path
end
it "allows creation from the #{template.name} template" do
fill_in("project_template_title", with: template.title)
fill_in("project_path", with: template.name)
page.within '#content-body' do
click_button "Create project"
end
expect(page).to have_content 'Import'
end
end
describe 'description' do describe 'description' do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:path) { project_path(project) } let(:path) { project_path(project) }
......
require 'spec_helper'
describe Gitlab::ProjectTemplate do
describe '.all' do
it 'returns a all templates' do
expected = [
described_class.new('rails', 'Ruby on Rails')
]
expect(described_class.all).to be_an(Array)
expect(described_class.all).to eq(expected)
end
end
describe '.find' do
subject { described_class.find(query) }
context 'when there is a match' do
let(:query) { :rails }
it { is_expected.to be_a(described_class) }
end
context 'when there is no match' do
let(:query) { 'no-match' }
it { is_expected.to be(nil) }
end
end
describe 'instance methods' do
subject { described_class.new('phoenix', 'Phoenix Framework') }
it { is_expected.to respond_to(:logo_path, :file, :template_archive) }
end
describe 'validate all templates' do
described_class.all.each do |template|
it "#{template.name} has a valid archive" do
archive = template.template_archive
logo = Rails.root.join("app/assets/images/#{template.logo_path}")
expect(File.exist?(archive)).to be(true)
expect(File.exist?(logo)).to be(true)
end
end
end
end
require 'spec_helper'
describe Projects::CreateFromTemplateService do
let(:user) { create(:user) }
let(:project_params) do
{
path: user.to_param,
template_title: 'rails'
}
end
subject { described_class.new(user, project_params) }
it 'calls the importer service' do
expect_any_instance_of(Projects::GitlabProjectsImporterService).to receive(:execute)
subject.execute
end
it 'returns the project thats created' do
project = subject.execute
expect(project).to be_saved
expect(project.import_status).to eq('scheduled')
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