Commit 4c8b6668 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'zj-project-templates' into 'master'

Allow projects to be started from a template

Closes #32420

See merge request !13108
parents 4e2b630d ae5003a3
......@@ -3,13 +3,13 @@ import $ from 'jquery';
// bootstrap jQuery plugins
import 'bootstrap-sass/assets/javascripts/bootstrap/affix';
import 'bootstrap-sass/assets/javascripts/bootstrap/alert';
import 'bootstrap-sass/assets/javascripts/bootstrap/button';
import 'bootstrap-sass/assets/javascripts/bootstrap/dropdown';
import 'bootstrap-sass/assets/javascripts/bootstrap/modal';
import 'bootstrap-sass/assets/javascripts/bootstrap/tab';
import 'bootstrap-sass/assets/javascripts/bootstrap/transition';
import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip';
import 'bootstrap-sass/assets/javascripts/bootstrap/popover';
import 'bootstrap-sass/assets/javascripts/bootstrap/button';
// custom jQuery functions
$.fn.extend({
......
import '../lib/utils/url_utility';
const bindEvents = () => {
const path = gl.utils.getParameterValues('path')[0];
// get the path url and append it in the inputS
$('.js-path-name').val(path);
};
document.addEventListener('DOMContentLoaded', bindEvents);
export default {
bindEvents,
};
let hasUserDefinedProjectPath = false;
const deriveProjectPathFromUrl = ($projectImportUrl, $projectPath) => {
if ($projectImportUrl.attr('disabled') || hasUserDefinedProjectPath) {
if (hasUserDefinedProjectPath) {
return;
}
......@@ -27,8 +27,6 @@ const deriveProjectPathFromUrl = ($projectImportUrl, $projectPath) => {
const bindEvents = () => {
const $newProjectForm = $('#new_project');
const importBtnTooltip = 'Please enter a valid project name.';
const $importBtnWrapper = $('.import_gitlab_project');
const $projectImportUrl = $('#project_import_url');
const $projectPath = $('#project_path');
......@@ -50,31 +48,15 @@ const bindEvents = () => {
$('.btn_import_gitlab_project').attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&path=${$projectPath.val()}`);
});
$('.btn_import_gitlab_project').attr('disabled', !$projectPath.val().trim().length);
$importBtnWrapper.attr('title', importBtnTooltip);
$newProjectForm.on('submit', () => {
$projectPath.val($projectPath.val().trim());
});
$projectPath.on('keyup', () => {
hasUserDefinedProjectPath = $projectPath.val().trim().length > 0;
if (hasUserDefinedProjectPath) {
$('.btn_import_gitlab_project').attr('disabled', false);
$importBtnWrapper.attr('title', '');
$importBtnWrapper.removeClass('has-tooltip');
} else {
$('.btn_import_gitlab_project').attr('disabled', true);
$importBtnWrapper.addClass('has-tooltip');
}
});
$projectImportUrl.disable();
$projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl, $projectPath));
$('.import_git').on('click', () => {
$projectImportUrl.attr('disabled', !$projectImportUrl.attr('disabled'));
});
};
document.addEventListener('DOMContentLoaded', bindEvents);
......
......@@ -629,3 +629,11 @@ $perf-bar-bucket-bg: #111;
$perf-bar-bucket-color: #ccc;
$perf-bar-bucket-box-shadow-from: rgba($white-light, .2);
$perf-bar-bucket-box-shadow-to: rgba($black, .25);
/*
Project Templates Icons
*/
$rails: #c00;
$node: #353535;
$java: #70ad51;
......@@ -7,7 +7,8 @@
}
.new_project,
.edit-project {
.edit-project,
.import-project {
.sharing-and-permissions {
.header {
......@@ -457,6 +458,7 @@ a.deploy-project-label {
}
}
.project-template,
.project-import {
.form-group {
margin-bottom: 5px;
......@@ -471,7 +473,44 @@ a.deploy-project-label {
.btn {
padding: 8px;
margin-left: 10px;
margin-right: 10px;
}
.blank-option {
min-width: 70px;
}
.btn-template-icon {
height: 24px;
width: inherit;
display: block;
margin: 0 auto 4px;
font-size: 24px;
@media (min-width: $screen-xs-max) {
top: 0;
}
}
@media (max-width: $screen-xs-max) {
.btn-template-icon {
display: inline-block;
height: 14px;
font-size: 14px;
margin: 0;
}
}
.icon-rails path {
fill: $rails;
}
.icon-node-express path {
fill: $node;
}
.icon-java-spring path {
fill: $java;
}
> div {
......@@ -481,6 +520,97 @@ a.deploy-project-label {
}
}
.project-templates-buttons .btn:last-child {
margin-right: 0;
}
.create-project-options {
display: flex;
@media (max-width: $screen-xs-max) {
display: block;
}
.first-column {
@media(min-width: $screen-xs-min) {
max-width: 50%;
padding-right: 30px;
}
@media(max-width: $screen-xs-max) {
max-width: 100%;
width: 100%;
}
}
.second-column {
@media(min-width: $screen-xs-min) {
width: 50%;
flex: 1;
padding-left: 30px;
position: relative;
}
@media(max-width: $screen-xs-max) {
max-width: 100%;
width: 100%;
padding-left: 0;
position: relative;
}
// Mobile
@media (max-width: $screen-xs-max) {
padding-top: 30px;
}
&::before {
content: "OR";
position: absolute;
left: 0;
top: 40%;
z-index: 10;
padding: 8px 0;
text-align: center;
background-color: $white-light;
color: $gl-text-color-tertiary;
transform: translateX(-50%);
font-size: 12px;
font-weight: bold;
line-height: 20px;
// Mobile
@media (max-width: $screen-xs-max) {
left: 50%;
top: 10px;
transform: translateY(-50%);
padding: 0 8px;
}
}
&::after {
content: "";
position: absolute;
background-color: $border-color;
bottom: 0;
left: 0;
right: auto;
height: 100%;
width: 1px;
top: 0;
// Mobile
@media (max-width: $screen-xs-max) {
top: 10px;
left: 10px;
right: 10px;
height: 1px;
width: auto;
}
}
}
}
.project-stats {
font-size: 0;
text-align: center;
......
......@@ -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." })
end
import_upload_path = Gitlab::ImportExport.import_upload_path(filename: project_params[:file].original_filename)
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
@project = ::Projects::GitlabProjectsImportService.new(current_user, project_params).execute
if @project.saved?
redirect_to(
......
......@@ -324,6 +324,7 @@ class ProjectsController < Projects::ApplicationController
:runners_token,
:tag_list,
:visibility_level,
:template_name,
project_feature_attributes: %i[
builds_access_level
......
......@@ -75,6 +75,7 @@ class Project < ActiveRecord::Base
attr_accessor :new_default_branch
attr_accessor :old_path_with_namespace
attr_accessor :template_name
attr_writer :pipeline_status
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_name]).file
GitlabProjectsImportService.new(@current_user, @params).execute
ensure
params[:file]&.close
end
end
end
......@@ -5,6 +5,10 @@ module Projects
end
def execute
if @params[:template_name]&.present?
return ::Projects::CreateFromTemplateService.new(current_user, params).execute
end
forked_from_project_id = params.delete(:forked_from_project_id)
import_data = params.delete(:import_data)
@skip_wiki = params.delete(:skip_wiki)
......
# 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 GitlabProjectsImportService
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: tmp_filename)
end
def tmp_filename
"#{SecureRandom.hex}_#{params[:path]}"
end
def file
params[:file]
end
end
end
- page_title "GitLab Import"
- header_title "Projects", root_path
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'project_import_gl'
%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}
= form_tag import_gitlab_project_path, class: 'new_project', multipart: true do
.row
.form-group.col-xs-12.col-sm-6
= label_tag :namespace_id, 'Project path', class: 'label-light'
.form-group
.input-group
- if current_user.can_select_namespace?
.input-group-addon
= root_url
= select_tag :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
%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: ''
- else
.input-group-addon.static-namespace
#{root_url}#{current_user.username}/
= hidden_field_tag :namespace_id, value: current_user.namespace_id
.form-group.col-xs-12.col-sm-6.project-path
= label_tag :path, 'Project name', class: 'label-light'
= text_field_tag :path, nil, placeholder: "my-awesome-project", class: "js-path-name form-control", tabindex: 2, autofocus: true, required: true
.form-actions
= submit_tag 'Import project', class: 'btn btn-create'
.row
.form-group.col-md-12
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.
.row
.form-group.col-sm-12
= hidden_field_tag :namespace_id, @namespace.id
= hidden_field_tag :path, @path
= label_tag :file, 'GitLab project export', class: 'label-light'
.form-group
= file_field_tag :file, class: ''
.row
.form-actions
= submit_tag 'Import project', class: 'btn btn-create'
= link_to 'Cancel', new_project_path, class: 'btn btn-cancel'
.project-templates-buttons.import-buttons{ data: { toggle: "buttons" } }
.btn.blank-option.active
%input{ type: "radio", autocomplete: "off", name: "project_templates", id: "blank", checked: "true" }
= icon('file-o', class: 'btn-template-icon')
Blank
- Gitlab::ProjectTemplate.all.each do |template|
.btn
%input{ type: "radio", autocomplete: "off", name: "project_templates", id: template.name }
= custom_icon(template.logo)
= template.title
......@@ -17,8 +17,68 @@
- if import_sources_enabled?
%p
Create or Import your project from popular Git services
.col-lg-9
.col-lg-9.js-toggle-container
= form_for @project, html: { class: 'new_project' } do |f|
.create-project-options
.first-column
.project-template
.form-group
= f.label :template_project, class: 'label-light' do
Create from template
= link_to icon('question-circle'), help_page_path("public_access/public_access"), aria: { label: "What’s included in a template?" }, title: "What’s included in a template?", class: 'has-tooltip', data: { placement: 'top'}
%div
= render 'project_templates', f: f
.second-column
- if import_sources_enabled?
.project-import
.form-group.clearfix
= f.label :visibility_level, class: 'label-light' do #the label here seems wrong
Import project from
.col-sm-12.import-buttons
%div
- if github_import_enabled?
= link_to new_import_github_path, class: 'btn import_github' do
= icon('github', text: 'GitHub')
%div
- if bitbucket_import_enabled?
= link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do
= icon('bitbucket', text: 'Bitbucket')
- unless bitbucket_import_configured?
= render 'bitbucket_import_modal'
%div
- if gitlab_import_enabled?
= link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do
= icon('gitlab', text: 'GitLab.com')
- unless gitlab_import_configured?
= render 'gitlab_import_modal'
%div
- if google_code_import_enabled?
= link_to new_import_google_code_path, class: 'btn import_google_code' do
= icon('google', text: 'Google Code')
%div
- if fogbugz_import_enabled?
= link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
= icon('bug', text: 'Fogbugz')
%div
- if gitea_import_enabled?
= link_to new_import_gitea_url, class: 'btn import_gitea' do
= custom_icon('go_logo')
Gitea
%div
- if git_import_enabled?
%button.btn.js-toggle-button.import_git{ type: "button" }
= icon('git', text: 'Repo by URL')
.import_gitlab_project.has-tooltip{ data: { container: 'body' } }
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
= icon('gitlab', text: 'GitLab export')
.row
.col-lg-12
.js-toggle-content.hide
%hr
= render "shared/import_form", f: f
%hr
.row
.form-group.col-xs-12.col-sm-6
= f.label :namespace_id, class: 'label-light' do
......@@ -45,53 +105,6 @@
Want to house several dependent projects under the same namespace?
= link_to "Create a group", new_group_path
- if import_sources_enabled?
.project-import.js-toggle-container
.form-group.clearfix
= f.label :visibility_level, class: 'label-light' do
Import project from
.col-sm-12.import-buttons
%div
- if github_import_enabled?
= link_to new_import_github_path, class: 'btn import_github' do
= icon('github', text: 'GitHub')
%div
- if bitbucket_import_enabled?
= link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do
= icon('bitbucket', text: 'Bitbucket')
- unless bitbucket_import_configured?
= render 'bitbucket_import_modal'
%div
- if gitlab_import_enabled?
= link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do
= icon('gitlab', text: 'GitLab.com')
- unless gitlab_import_configured?
= render 'gitlab_import_modal'
%div
- if google_code_import_enabled?
= link_to new_import_google_code_path, class: 'btn import_google_code' do
= icon('google', text: 'Google Code')
%div
- if fogbugz_import_enabled?
= link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
= icon('bug', text: 'FogBugz')
%div
- if gitea_import_enabled?
= link_to new_import_gitea_url, class: 'btn import_gitea' do
= custom_icon('go_logo')
Gitea
%div
- if git_import_enabled?
%button.btn.js-toggle-button.import_git{ type: "button" }
= icon('git', text: 'Repo by URL')
.import_gitlab_project.has-tooltip{ data: { container: 'body' } }
- if gitlab_project_import_enabled?
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
= icon('gitlab', text: 'GitLab export')
.js-toggle-content.hide
= render "shared/import_form", f: f
.form-group
= f.label :description, class: 'label-light' do
Project description
......
.form-group.import-url-data
= f.label :import_url, class: 'control-label' do
= f.label :import_url, class: 'label-light' do
%span Git repository URL
.col-sm-10
= f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git'
.well.prepend-top-20
%ul
%li
The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.
%li
If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.
%li
The import will time out after 15 minutes. For repositories that take longer, use a clone/push combination.
%li
To migrate an SVN repository, check out #{link_to "this document", help_page_path('workflow/importing/migrating_from_svn')}.
= f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git'
.well.prepend-top-20
%ul
%li
The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.
%li
If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.
%li
The import will time out after 15 minutes. For repositories that take longer, use a clone/push combination.
%li
To migrate an SVN repository, check out #{link_to "this document", help_page_path('workflow/importing/migrating_from_svn')}.
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" class="btn-template-icon icon-java-spring">
<g fill="none" fill-rule="evenodd">
<rect width="32" height="32"/>
<path fill="#70AD51" d="M5.46647617,27.9932117 C6.0517027,28.4658996 6.91159892,28.3777063 7.38425926,27.7914452 C7.85922261,27.2048452 7.76991326,26.3449044 7.18398981,25.8699411 C6.59874295,25.3956543 5.74015536,25.4869934 5.26383884,26.0722403 C4.81393367,26.6267596 4.87238621,27.4284565 5.37913494,27.9159868 L5.11431334,27.6818383 C1.97157151,24.7616933 0,20.5966301 0,15.9782542 C0,7.16842834 7.16775175,0 15.9796074,0 C20.4586065,0 24.5113565,1.8565519 27.4145869,4.8362365 C28.0749348,3.93840692 28.6466499,2.93435335 29.115524,1.82069284 C31.1513712,7.93770658 32.3482517,13.0811131 31.909824,17.1311567 C31.3178113,25.4044499 24.4017495,31.9585382 15.9796074,31.9585382 C12.0682639,31.9585382 8.48438805,30.5444735 5.7042963,28.2034861 L5.46647617,27.9932117 Z M29.0471888,23.0106888 C33.0546075,17.6737787 30.8211972,9.04527781 28.9612624,3.529749 C27.3029502,6.98304378 23.2217836,9.62375882 19.6981239,10.4613722 C16.3950312,11.2482417 13.4715032,10.6021021 10.4153644,11.7780085 C3.44517575,14.457289 3.55613585,22.7698242 7.39373146,24.6365249 C7.39711439,24.6392312 7.62444728,24.7616933 7.62174094,24.7576338 C7.62309411,24.7562806 13.2658211,23.6358542 16.3862356,22.4843049 C20.9450718,20.7996058 25.9524846,16.6494275 27.5986182,11.8273993 C26.723116,16.8415779 22.4179995,21.6669891 18.093262,23.8828081 C15.7908399,25.0648038 14.0005934,25.3279957 10.2123886,26.6385428 C9.74892722,26.798217 9.38492397,26.9538318 9.38492397,26.9538318 C10.3463526,26.7948341 11.301692,26.7420604 11.301692,26.7420604 C16.6954354,26.4869875 25.1087819,28.2582896 29.0471888,23.0106888 Z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="27" height="32" viewBox="0 0 27 32" class="btn-template-icon icon-node-express">
<g fill="none" fill-rule="evenodd" transform="translate(-3)">
<rect width="32" height="32"/>
<path fill="#353535" d="M4.19170065,16.2667139 C4.23142421,18.3323387 4.47969269,20.2489714 4.93651356,22.0166696 C5.39333443,23.7843677 6.09841693,25.3236323 7.05178222,26.6345096 C8.00514751,27.9453869 9.23655921,28.9781838 10.7460543,29.7329313 C12.2555493,30.4876788 14.1026668,30.8650469 16.2874623,30.8650469 C19.5050701,30.8650469 22.1764391,30.0209341 24.3016492,28.3326831 C26.4268593,26.644432 27.7476477,24.1120935 28.2640539,20.7355914 L29.4557545,20.7355914 C29.0187954,24.3107112 27.6086304,27.0813875 25.2252172,29.0477034 C22.841804,31.0140194 19.9023051,31.9971626 16.4066324,31.9971626 C14.0232191,32.0368861 11.9874175,31.659518 10.2991665,30.8650469 C8.61091547,30.0705759 7.23054269,28.9484023 6.15800673,27.4984926 C5.08547078,26.0485829 4.29101162,24.3404957 3.77460543,22.3741798 C3.25819923,20.4078639 3,18.2926164 3,16.0283738 C3,13.4860664 3.3773681,11.2218578 4.13211562,9.23568007 C4.88686314,7.24950238 5.87993709,5.57120741 7.11136726,4.20074481 C8.34279742,2.8302822 9.77282391,1.78755456 11.4014896,1.07253059 C13.0301553,0.357506621 14.6985195,0 16.4066324,0 C18.7900456,0 20.8457087,0.456814016 22.5736832,1.37045575 C24.3016578,2.28409749 25.7118228,3.4956477 26.8042206,5.00514275 C27.8966183,6.51463779 28.6910775,8.24258646 29.1876219,10.1890406 C29.6841663,12.1354947 29.8927118,14.1613656 29.8132647,16.2667139 L4.19170065,16.2667139 Z M28.6215641,15.0750133 C28.6215641,13.2080062 28.3633648,11.4304039 27.8469586,9.74215285 C27.3305524,8.05390181 26.5658855,6.57422163 25.5529349,5.30306791 C24.5399843,4.03191419 23.2787803,3.0289095 21.7692853,2.29402376 C20.2597903,1.55913801 18.5119801,1.19170065 16.5258024,1.19170065 C14.8574132,1.19170065 13.2982871,1.50948432 11.8483774,2.14506118 C10.3984676,2.78063804 9.12733299,3.70419681 8.03493526,4.9157652 C6.94253754,6.12733359 6.05870172,7.58715229 5.38340131,9.2952651 C4.70810089,11.0033779 4.31087132,12.9299414 4.19170065,15.0750133 L28.6215641,15.0750133 Z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="20" viewBox="0 0 32 20" class="btn-template-icon icon-rails">
<g fill="none" fill-rule="evenodd" transform="translate(0 -6)">
<rect width="32" height="32"/>
<path fill="#C00" fill-rule="nonzero" d="M0.984615385,25.636044 C0.984615385,25.636044 1.40659341,21.4725275 4.36043956,16.5494505 C7.31428571,11.6263736 12.3498901,7.8989011 16.4430769,7.53318681 C24.5872527,6.71736264 31.9015385,14.0175824 31.9015385,14.0175824 C31.9015385,14.0175824 31.6624176,14.1863736 31.4092308,14.3973626 C23.4197802,8.48967033 18.5389011,11.2747253 17.0057143,12.0202198 C9.97274725,15.9446154 12.0967033,25.636044 12.0967033,25.636044 L0.984615385,25.636044 Z M24.1371429,8.32087912 C23.687033,8.13802198 23.2369231,7.96923077 22.7727473,7.81450549 L22.829011,6.88615385 C23.7151648,7.13934066 24.0668132,7.30813187 24.1934066,7.37846154 L24.1371429,8.32087912 Z M22.8008791,11.3028571 C23.250989,11.330989 23.7151648,11.3872527 24.1934066,11.4857143 L24.1371429,12.3578022 C23.672967,12.2593407 23.2087912,12.2030769 22.7446154,12.189011 L22.8008791,11.3028571 Z M17.5964835,6.91428571 C17.1885714,6.91428571 16.7806593,6.92835165 16.3727473,6.97054945 L16.1054945,6.14065934 C16.5696703,6.0843956 17.0197802,6.05626374 17.4558242,6.05626374 L17.7371429,6.91428571 C17.6949451,6.91428571 17.6386813,6.91428571 17.5964835,6.91428571 Z M18.2716484,12.0905495 C18.6232967,11.9358242 19.0312088,11.7810989 19.5094505,11.6404396 L19.8189011,12.5687912 C19.410989,12.6953846 19.0030769,12.8641758 18.5951648,13.0610989 L18.2716484,12.0905495 Z M11.8857143,8.39120879 C11.52,8.57406593 11.1683516,8.78505495 10.8026374,9.01010989 L10.1556044,8.02549451 C10.5353846,7.80043956 10.9010989,7.60351648 11.2527473,7.42065934 L11.8857143,8.39120879 Z M14.7692308,14.7208791 C15.0224176,14.3973626 15.3178022,14.0738462 15.6413187,13.7784615 L16.2742857,14.7349451 C15.9648352,15.0584615 15.6835165,15.381978 15.4443956,15.7336264 L14.7692308,14.7208791 Z M12.7296703,19.2501099 C12.8421978,18.7437363 12.9687912,18.2232967 13.1516484,17.7028571 L14.1643956,18.5046154 C14.0237363,19.0531868 13.9252747,19.6017582 13.869011,20.1503297 L12.7296703,19.2501099 Z M6.56879121,12.5687912 C6.23120879,12.9204396 5.90769231,13.3002198 5.61230769,13.68 L4.52923077,12.7516484 C4.85274725,12.4 5.2043956,12.0483516 5.57010989,11.6967033 L6.56879121,12.5687912 Z M2.32087912,18.8562637 C2.09582418,19.3767033 1.80043956,20.0659341 1.61758242,20.5441758 L0,19.9534066 C0.140659341,19.5736264 0.436043956,18.8703297 0.703296703,18.2654945 L2.32087912,18.8562637 Z M12.5186813,22.8228571 L14.0378022,23.3714286 C14.1221978,24.0325275 14.2487912,24.6514286 14.3753846,25.2 L12.6874725,24.5951648 C12.6171429,24.1731868 12.5468132,23.5683516 12.5186813,22.8228571 Z"/>
</g>
</svg>
---
title: Projects can be created from templates
merge_request: 13108
author:
......@@ -60,6 +60,7 @@ var config = {
pipelines_details: './pipelines/pipeline_details_bundle.js',
pipelines_times: './pipelines/pipelines_times.js',
profile: './profile/profile_bundle.js',
project_import_gl: './projects/project_import_gitlab_project.js',
project_new: './projects/project_new.js',
prometheus_metrics: './prometheus_metrics',
protected_branches: './protected_branches',
......
......@@ -146,3 +146,20 @@ If new emoji are added, the spritesheet may change size. To compensate for
such changes, first generate the `emoji.png` spritesheet with the above Rake
task, then check the dimensions of the new spritesheet and update the
`SPRITESHEET_WIDTH` and `SPRITESHEET_HEIGHT` constants accordingly.
## Updating project templates
Starting a project from a template needs this project to be exported. On a
up to date master branch with run:
```
gdk run db
# In a new terminal window
bundle exec rake gitlab:update_project_templates
git checkout -b update-project-templates
git add vendor/project_templates
git commit
git push -u origin update-project-templates
```
Now create a merge request and merge that to master.
module Gitlab
class ProjectTemplate
attr_reader :title, :name
def initialize(name, title)
@name, @title = name, title
end
alias_method :logo, :name
def file
archive_path.open
end
def archive_path
Rails.root.join("vendor/project_templates/#{name}.tar.gz")
end
def clone_url
"https://gitlab.com/gitlab-org/project-templates/#{name}.git"
end
def ==(other)
name == other.name && title == other.title
end
TEMPLATES_TABLE = [
ProjectTemplate.new('rails', 'Ruby on Rails')
].freeze
class << self
def all
TEMPLATES_TABLE
end
def find(name)
all.find { |template| template.name == name.to_s }
end
def archive_directory
Rails.root.join("vendor_directory/project_templates")
end
end
end
end
......@@ -4,6 +4,55 @@ namespace :gitlab do
TEMPLATE_DATA.each { |template| update(template) }
end
desc "GitLab | Update project templates"
task :update_project_templates do
if Rails.env.production?
puts "This rake task is not meant fo production instances".red
exit(1)
end
admin = User.find_by(admin: true)
unless admin
puts "No admin user could be found".red
exit(1)
end
Gitlab::ProjectTemplate.all.each do |template|
params = {
import_url: template.clone_url,
namespace_id: admin.namespace.id,
path: template.title,
skip_wiki: true
}
puts "Creating project for #{template.name}"
project = Projects::CreateService.new(admin, params).execute
loop do
if project.finished?
puts "Import finished for #{template.name}"
break
end
if project.failed?
puts "Failed to import from #{project_params[:import_url]}".red
exit(1)
end
puts "Waiting for the import to finish"
sleep(5)
project.reload
end
Projects::ImportExport::ExportService.new(project, admin).execute
FileUtils.cp(project.export_project_path, template.archive_path)
Projects::DestroyService.new(admin, project).execute
puts "Exported #{template.name}".green
end
puts "Done".green
end
def update(template)
sub_dir = template.repo_url.match(/([A-Za-z-]+)\.git\z/)[1]
dir = File.join(vendor_directory, sub_dir)
......
......@@ -29,8 +29,9 @@ feature 'Import/Export - project import integration test', js: true do
fill_in :project_path, with: 'test-project-path', visible: true
click_link 'GitLab export'
expect(page).to have_content('GitLab project export')
expect(page).to have_content('Import an exported GitLab project')
expect(URI.parse(current_url).query).to eq("namespace_id=#{namespace.id}&path=test-project-path")
expect(Gitlab::ImportExport).to receive(:import_upload_path).with(filename: /\A\h{32}_test-project-path\z/).and_call_original
attach_file('file', file)
......@@ -60,17 +61,6 @@ feature 'Import/Export - project import integration test', js: true do
expect(page).to have_content('Project could not be imported')
end
end
scenario 'project with no name' do
create(:project, namespace: namespace)
visit new_project_path
select2(namespace.id, from: '#project_namespace_id')
# Check for tooltip disabled import button
expect(find('.import_gitlab_project')['title']).to eq('Please enter a valid project name.')
end
end
context 'when limited to the default user namespace' do
......
require 'spec_helper'
feature 'Project' 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 templates" do
page.choose(template.name)
fill_in("project_path", with: template.name)
page.within '#content-body' do
click_button "Create project"
end
expect(page).to have_content 'This project Loading..'
end
end
describe 'description' do
let(:project) { create(:project, :repository) }
let(:path) { project_path(project) }
......
import projectImportGitlab from '~/projects/project_import_gitlab_project';
describe('Import Gitlab project', () => {
let projectName;
beforeEach(() => {
projectName = 'project';
window.history.pushState({}, null, `?path=${projectName}`);
setFixtures(`
<input class="js-path-name" />
`);
projectImportGitlab.bindEvents();
});
afterEach(() => {
window.history.pushState({}, null, '');
});
describe('path name', () => {
it('should fill in the project name derived from the previously filled project name', () => {
expect(document.querySelector('.js-path-name').value).toEqual(projectName);
});
});
});
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, :file, :archive_path) }
end
describe 'validate all templates' do
set(:admin) { create(:admin) }
described_class.all.each do |template|
it "#{template.name} has a valid archive" do
archive = template.archive_path
expect(File.exist?(archive)).to be(true)
end
context 'with valid parameters' do
it 'can be imported' do
params = {
template_name: template.name,
namespace_id: admin.namespace.id,
path: template.name
}
project = Projects::CreateFromTemplateService.new(admin, params).execute
expect(project).to be_valid
expect(project).to be_persisted
end
end
end
end
end
require 'spec_helper'
describe Projects::CreateFromTemplateService do
let(:user) { create(:user) }
let(:project_params) do
{
path: user.to_param,
template_name: 'rails'
}
end
subject { described_class.new(user, project_params) }
it 'calls the importer service' do
expect_any_instance_of(Projects::GitlabProjectsImportService).to receive(:execute)
subject.execute
end
it 'returns the project thats created' do
project = subject.execute
expect(project).to be_saved
expect(project.scheduled?).to be(true)
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