Merge branch 'issue_18656' into 'master'
Load issue templates from repository part of #18656 ## Does this MR meet the acceptance criteria? - [x] [CHANGELOG](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG) entry added - [x] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md) - [x] API support added - Tests - [x] Added for this feature/bug - [x] All builds are passing - [x] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides) - [x] Branch has no merge conflicts with `master` (if you do - rebase it please) - [x] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits) See merge request !4981
This commit is contained in:
commit
f7e08ad0de
|
@ -40,6 +40,7 @@ v 8.11.0 (unreleased)
|
|||
- Various redundant database indexes have been removed
|
||||
- Update `timeago` plugin to use multiple string/locale settings
|
||||
- Remove unused images (ClemMakesApps)
|
||||
- Get issue and merge request description templates from repositories
|
||||
- Limit git rev-list output count to one in forced push check
|
||||
- Show deployment status on merge requests with external URLs
|
||||
- Clean up unused routes (Josef Strzibny)
|
||||
|
|
|
@ -9,10 +9,11 @@
|
|||
licensePath: "/api/:version/licenses/:key",
|
||||
gitignorePath: "/api/:version/gitignores/:key",
|
||||
gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key",
|
||||
issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key",
|
||||
|
||||
group: function(group_id, callback) {
|
||||
var url;
|
||||
url = Api.buildUrl(Api.groupPath);
|
||||
url = url.replace(':id', group_id);
|
||||
var url = Api.buildUrl(Api.groupPath)
|
||||
.replace(':id', group_id);
|
||||
return $.ajax({
|
||||
url: url,
|
||||
data: {
|
||||
|
@ -24,8 +25,7 @@
|
|||
});
|
||||
},
|
||||
groups: function(query, skip_ldap, callback) {
|
||||
var url;
|
||||
url = Api.buildUrl(Api.groupsPath);
|
||||
var url = Api.buildUrl(Api.groupsPath);
|
||||
return $.ajax({
|
||||
url: url,
|
||||
data: {
|
||||
|
@ -39,8 +39,7 @@
|
|||
});
|
||||
},
|
||||
namespaces: function(query, callback) {
|
||||
var url;
|
||||
url = Api.buildUrl(Api.namespacesPath);
|
||||
var url = Api.buildUrl(Api.namespacesPath);
|
||||
return $.ajax({
|
||||
url: url,
|
||||
data: {
|
||||
|
@ -54,8 +53,7 @@
|
|||
});
|
||||
},
|
||||
projects: function(query, order, callback) {
|
||||
var url;
|
||||
url = Api.buildUrl(Api.projectsPath);
|
||||
var url = Api.buildUrl(Api.projectsPath);
|
||||
return $.ajax({
|
||||
url: url,
|
||||
data: {
|
||||
|
@ -70,9 +68,8 @@
|
|||
});
|
||||
},
|
||||
newLabel: function(project_id, data, callback) {
|
||||
var url;
|
||||
url = Api.buildUrl(Api.labelsPath);
|
||||
url = url.replace(':id', project_id);
|
||||
var url = Api.buildUrl(Api.labelsPath)
|
||||
.replace(':id', project_id);
|
||||
data.private_token = gon.api_token;
|
||||
return $.ajax({
|
||||
url: url,
|
||||
|
@ -86,9 +83,8 @@
|
|||
});
|
||||
},
|
||||
groupProjects: function(group_id, query, callback) {
|
||||
var url;
|
||||
url = Api.buildUrl(Api.groupProjectsPath);
|
||||
url = url.replace(':id', group_id);
|
||||
var url = Api.buildUrl(Api.groupProjectsPath)
|
||||
.replace(':id', group_id);
|
||||
return $.ajax({
|
||||
url: url,
|
||||
data: {
|
||||
|
@ -102,8 +98,8 @@
|
|||
});
|
||||
},
|
||||
licenseText: function(key, data, callback) {
|
||||
var url;
|
||||
url = Api.buildUrl(Api.licensePath).replace(':key', key);
|
||||
var url = Api.buildUrl(Api.licensePath)
|
||||
.replace(':key', key);
|
||||
return $.ajax({
|
||||
url: url,
|
||||
data: data
|
||||
|
@ -112,19 +108,32 @@
|
|||
});
|
||||
},
|
||||
gitignoreText: function(key, callback) {
|
||||
var url;
|
||||
url = Api.buildUrl(Api.gitignorePath).replace(':key', key);
|
||||
var url = Api.buildUrl(Api.gitignorePath)
|
||||
.replace(':key', key);
|
||||
return $.get(url, function(gitignore) {
|
||||
return callback(gitignore);
|
||||
});
|
||||
},
|
||||
gitlabCiYml: function(key, callback) {
|
||||
var url;
|
||||
url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key);
|
||||
var url = Api.buildUrl(Api.gitlabCiYmlPath)
|
||||
.replace(':key', key);
|
||||
return $.get(url, function(file) {
|
||||
return callback(file);
|
||||
});
|
||||
},
|
||||
issueTemplate: function(namespacePath, projectPath, key, type, callback) {
|
||||
var url = Api.buildUrl(Api.issuableTemplatePath)
|
||||
.replace(':key', key)
|
||||
.replace(':type', type)
|
||||
.replace(':project_path', projectPath)
|
||||
.replace(':namespace_path', namespacePath);
|
||||
$.ajax({
|
||||
url: url,
|
||||
dataType: 'json'
|
||||
}).done(function(file) {
|
||||
callback(null, file);
|
||||
}).error(callback);
|
||||
},
|
||||
buildUrl: function(url) {
|
||||
if (gon.relative_url_root != null) {
|
||||
url = gon.relative_url_root + url;
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
/*= require date.format */
|
||||
/*= require_directory ./behaviors */
|
||||
/*= require_directory ./blob */
|
||||
/*= require_directory ./templates */
|
||||
/*= require_directory ./commit */
|
||||
/*= require_directory ./extensions */
|
||||
/*= require_directory ./lib/utils */
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
}
|
||||
this.onClick = bind(this.onClick, this);
|
||||
this.dropdown = opts.dropdown, this.data = opts.data, this.pattern = opts.pattern, this.wrapper = opts.wrapper, this.editor = opts.editor, this.fileEndpoint = opts.fileEndpoint, this.$input = (ref = opts.$input) != null ? ref : $('#file_name');
|
||||
this.dropdownIcon = $('.fa-chevron-down', this.dropdown);
|
||||
this.buildDropdown();
|
||||
this.bindEvents();
|
||||
this.onFilenameUpdate();
|
||||
|
@ -60,11 +61,26 @@
|
|||
return this.requestFile(item);
|
||||
};
|
||||
|
||||
TemplateSelector.prototype.requestFile = function(item) {};
|
||||
TemplateSelector.prototype.requestFile = function(item) {
|
||||
// This `requestFile` method is an abstract method that should
|
||||
// be added by all subclasses.
|
||||
};
|
||||
|
||||
TemplateSelector.prototype.requestFileSuccess = function(file) {
|
||||
TemplateSelector.prototype.requestFileSuccess = function(file, skipFocus) {
|
||||
this.editor.setValue(file.content, 1);
|
||||
return this.editor.focus();
|
||||
if (!skipFocus) this.editor.focus();
|
||||
};
|
||||
|
||||
TemplateSelector.prototype.startLoadingSpinner = function() {
|
||||
this.dropdownIcon
|
||||
.addClass('fa-spinner fa-spin')
|
||||
.removeClass('fa-chevron-down');
|
||||
};
|
||||
|
||||
TemplateSelector.prototype.stopLoadingSpinner = function() {
|
||||
this.dropdownIcon
|
||||
.addClass('fa-chevron-down')
|
||||
.removeClass('fa-spinner fa-spin');
|
||||
};
|
||||
|
||||
return TemplateSelector;
|
||||
|
|
|
@ -55,6 +55,7 @@
|
|||
shortcut_handler = new ShortcutsNavigation();
|
||||
new GLForm($('.issue-form'));
|
||||
new IssuableForm($('.issue-form'));
|
||||
new IssuableTemplateSelectors();
|
||||
break;
|
||||
case 'projects:merge_requests:new':
|
||||
case 'projects:merge_requests:edit':
|
||||
|
@ -62,6 +63,7 @@
|
|||
shortcut_handler = new ShortcutsNavigation();
|
||||
new GLForm($('.merge-request-form'));
|
||||
new IssuableForm($('.merge-request-form'));
|
||||
new IssuableTemplateSelectors();
|
||||
break;
|
||||
case 'projects:tags:new':
|
||||
new ZenMode();
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*= require ../blob/template_selector */
|
||||
|
||||
((global) => {
|
||||
class IssuableTemplateSelector extends TemplateSelector {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.projectPath = this.dropdown.data('project-path');
|
||||
this.namespacePath = this.dropdown.data('namespace-path');
|
||||
this.issuableType = this.wrapper.data('issuable-type');
|
||||
this.titleInput = $(`#${this.issuableType}_title`);
|
||||
|
||||
let initialQuery = {
|
||||
name: this.dropdown.data('selected')
|
||||
};
|
||||
|
||||
if (initialQuery.name) this.requestFile(initialQuery);
|
||||
|
||||
$('.reset-template', this.dropdown.parent()).on('click', () => {
|
||||
if (this.currentTemplate) this.setInputValueToTemplateContent();
|
||||
});
|
||||
}
|
||||
|
||||
requestFile(query) {
|
||||
this.startLoadingSpinner();
|
||||
Api.issueTemplate(this.namespacePath, this.projectPath, query.name, this.issuableType, (err, currentTemplate) => {
|
||||
this.currentTemplate = currentTemplate;
|
||||
if (err) return; // Error handled by global AJAX error handler
|
||||
this.stopLoadingSpinner();
|
||||
this.setInputValueToTemplateContent();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setInputValueToTemplateContent() {
|
||||
// `this.requestFileSuccess` sets the value of the description input field
|
||||
// to the content of the template selected.
|
||||
if (this.titleInput.val() === '') {
|
||||
// If the title has not yet been set, focus the title input and
|
||||
// skip focusing the description input by setting `true` as the 2nd
|
||||
// argument to `requestFileSuccess`.
|
||||
this.requestFileSuccess(this.currentTemplate, true);
|
||||
this.titleInput.focus();
|
||||
} else {
|
||||
this.requestFileSuccess(this.currentTemplate);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
global.IssuableTemplateSelector = IssuableTemplateSelector;
|
||||
})(window);
|
|
@ -0,0 +1,29 @@
|
|||
((global) => {
|
||||
class IssuableTemplateSelectors {
|
||||
constructor(opts = {}) {
|
||||
this.$dropdowns = opts.$dropdowns || $('.js-issuable-selector');
|
||||
this.editor = opts.editor || this.initEditor();
|
||||
|
||||
this.$dropdowns.each((i, dropdown) => {
|
||||
let $dropdown = $(dropdown);
|
||||
new IssuableTemplateSelector({
|
||||
pattern: /(\.md)/,
|
||||
data: $dropdown.data('data'),
|
||||
wrapper: $dropdown.closest('.js-issuable-selector-wrap'),
|
||||
dropdown: $dropdown,
|
||||
editor: this.editor
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initEditor() {
|
||||
let editor = $('.markdown-area');
|
||||
// Proxy ace-editor's .setValue to jQuery's .val
|
||||
editor.setValue = editor.val;
|
||||
editor.getValue = editor.val;
|
||||
return editor;
|
||||
}
|
||||
}
|
||||
|
||||
global.IssuableTemplateSelectors = IssuableTemplateSelectors;
|
||||
})(window);
|
|
@ -56,9 +56,13 @@
|
|||
position: absolute;
|
||||
top: 50%;
|
||||
right: 6px;
|
||||
margin-top: -4px;
|
||||
margin-top: -6px;
|
||||
color: $dropdown-toggle-icon-color;
|
||||
font-size: 10px;
|
||||
&.fa-spinner {
|
||||
font-size: 16px;
|
||||
margin-top: -8px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover, {
|
||||
|
@ -406,6 +410,7 @@
|
|||
font-size: 14px;
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -395,3 +395,12 @@
|
|||
display: inline-block;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.js-issuable-selector-wrap {
|
||||
.js-issuable-selector {
|
||||
width: 100%;
|
||||
}
|
||||
@media (max-width: $screen-sm-max) {
|
||||
margin-bottom: $gl-padding;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
class Projects::TemplatesController < Projects::ApplicationController
|
||||
before_action :authenticate_user!, :get_template_class
|
||||
|
||||
def show
|
||||
template = @template_type.find(params[:key], project)
|
||||
|
||||
respond_to do |format|
|
||||
format.json { render json: template.to_json }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_template_class
|
||||
template_types = { issue: Gitlab::Template::IssueTemplate, merge_request: Gitlab::Template::MergeRequestTemplate }.with_indifferent_access
|
||||
@template_type = template_types[params[:template_type]]
|
||||
render json: [], status: 404 unless @template_type
|
||||
end
|
||||
end
|
|
@ -182,17 +182,42 @@ module BlobHelper
|
|||
}
|
||||
end
|
||||
|
||||
def selected_template(issuable)
|
||||
templates = issuable_templates(issuable)
|
||||
params[:issuable_template] if templates.include?(params[:issuable_template])
|
||||
end
|
||||
|
||||
def can_add_template?(issuable)
|
||||
names = issuable_templates(issuable)
|
||||
names.empty? && can?(current_user, :push_code, @project) && !@project.private?
|
||||
end
|
||||
|
||||
def merge_request_template_names
|
||||
@merge_request_templates ||= Gitlab::Template::MergeRequestTemplate.dropdown_names(ref_project)
|
||||
end
|
||||
|
||||
def issue_template_names
|
||||
@issue_templates ||= Gitlab::Template::IssueTemplate.dropdown_names(ref_project)
|
||||
end
|
||||
|
||||
def issuable_templates(issuable)
|
||||
@issuable_templates ||=
|
||||
if issuable.is_a?(Issue)
|
||||
issue_template_names
|
||||
elsif issuable.is_a?(MergeRequest)
|
||||
merge_request_template_names
|
||||
end
|
||||
end
|
||||
|
||||
def ref_project
|
||||
@ref_project ||= @target_project || @project
|
||||
end
|
||||
|
||||
def gitignore_names
|
||||
@gitignore_names ||=
|
||||
Gitlab::Template::Gitignore.categories.keys.map do |k|
|
||||
[k, Gitlab::Template::Gitignore.by_category(k).map { |t| { name: t.name } }]
|
||||
end.to_h
|
||||
@gitignore_names ||= Gitlab::Template::GitignoreTemplate.dropdown_names
|
||||
end
|
||||
|
||||
def gitlab_ci_ymls
|
||||
@gitlab_ci_ymls ||=
|
||||
Gitlab::Template::GitlabCiYml.categories.keys.map do |k|
|
||||
[k, Gitlab::Template::GitlabCiYml.by_category(k).map { |t| { name: t.name } }]
|
||||
end.to_h
|
||||
@gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names
|
||||
end
|
||||
end
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
.filter-item.inline
|
||||
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
|
||||
.filter-item.inline.labels-filter
|
||||
= render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, show_footer: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
|
||||
= render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
|
||||
.filter-item.inline
|
||||
= dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do
|
||||
%ul
|
||||
|
|
|
@ -2,7 +2,22 @@
|
|||
|
||||
.form-group
|
||||
= f.label :title, class: 'control-label'
|
||||
.col-sm-10
|
||||
|
||||
- issuable_template_names = issuable_templates(issuable)
|
||||
|
||||
- if issuable_template_names.any?
|
||||
.col-sm-3.col-lg-2
|
||||
.js-issuable-selector-wrap{ data: { issuable_type: issuable.class.to_s.underscore.downcase } }
|
||||
- title = selected_template(issuable) || "Choose a template"
|
||||
|
||||
= dropdown_tag(title, options: { toggle_class: 'js-issuable-selector',
|
||||
title: title, filter: true, placeholder: 'Filter', footer_content: true,
|
||||
data: { data: issuable_template_names, field_name: 'issuable_template', selected: selected_template(issuable), project_path: @project.path, namespace_path: @project.namespace.path } } ) do
|
||||
%ul.dropdown-footer-list
|
||||
%li
|
||||
%a.reset-template
|
||||
Reset template
|
||||
%div{ class: issuable_template_names.any? ? 'col-sm-7 col-lg-8' : 'col-sm-10' }
|
||||
= f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off',
|
||||
class: 'form-control pad', required: true
|
||||
|
||||
|
@ -23,6 +38,13 @@
|
|||
to prevent a
|
||||
%strong Work In Progress
|
||||
merge request from being merged before it's ready.
|
||||
|
||||
- if can_add_template?(issuable)
|
||||
%p.help-block
|
||||
Add
|
||||
= link_to "issuable templates", help_page_path('workflow/description_templates')
|
||||
to help your contributors communicate effectively!
|
||||
|
||||
.form-group.detail-page-description
|
||||
= f.label :description, 'Description', class: 'control-label'
|
||||
.col-sm-10
|
||||
|
|
|
@ -528,6 +528,11 @@ Rails.application.routes.draw do
|
|||
put '/update/*id', to: 'blob#update', constraints: { id: /.+/ }, as: 'update_blob'
|
||||
post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob'
|
||||
|
||||
#
|
||||
# Templates
|
||||
#
|
||||
get '/templates/:template_type/:key' => 'templates#show', as: :template
|
||||
|
||||
scope do
|
||||
get(
|
||||
'/blob/*id/diff',
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
- [Share projects with other groups](share_projects_with_other_groups.md)
|
||||
- [Web Editor](web_editor.md)
|
||||
- [Releases](releases.md)
|
||||
- [Issuable Templates](issuable_templates.md)
|
||||
- [Milestones](milestones.md)
|
||||
- [Merge Requests](merge_requests.md)
|
||||
- [Revert changes](revert_changes.md)
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
# Description templates
|
||||
|
||||
Description templates allow you to define context-specific templates for issue and merge request description fields for your project. When in use, users that create a new issue or merge request can select a description template to help them communicate with other contributors effectively.
|
||||
|
||||
Every GitLab project can define its own set of description templates as they are added to the root directory of a GitLab project's repository.
|
||||
|
||||
Description templates are written in markdown _(`.md`)_ and stored in your projects repository under the `/.gitlab/issue_templates/` and `/.gitlab/merge_request_templates/` directories.
|
||||
|
||||
![Description templates](img/description_templates.png)
|
||||
|
||||
_Example:_
|
||||
`/.gitlab/issue_templates/bug.md` will enable the `bug` dropdown option for new issues. When `bug` is selected, the content from the `bug.md` template file will be copied to the issue description field.
|
Binary file not shown.
After Width: | Height: | Size: 56 KiB |
|
@ -1,21 +1,28 @@
|
|||
module API
|
||||
class Templates < Grape::API
|
||||
TEMPLATE_TYPES = {
|
||||
gitignores: Gitlab::Template::Gitignore,
|
||||
gitlab_ci_ymls: Gitlab::Template::GitlabCiYml
|
||||
GLOBAL_TEMPLATE_TYPES = {
|
||||
gitignores: Gitlab::Template::GitignoreTemplate,
|
||||
gitlab_ci_ymls: Gitlab::Template::GitlabCiYmlTemplate
|
||||
}.freeze
|
||||
|
||||
TEMPLATE_TYPES.each do |template, klass|
|
||||
helpers do
|
||||
def render_response(template_type, template)
|
||||
not_found!(template_type.to_s.singularize) unless template
|
||||
present template, with: Entities::Template
|
||||
end
|
||||
end
|
||||
|
||||
GLOBAL_TEMPLATE_TYPES.each do |template_type, klass|
|
||||
# Get the list of the available template
|
||||
#
|
||||
# Example Request:
|
||||
# GET /gitignores
|
||||
# GET /gitlab_ci_ymls
|
||||
get template.to_s do
|
||||
get template_type.to_s do
|
||||
present klass.all, with: Entities::TemplatesList
|
||||
end
|
||||
|
||||
# Get the text for a specific template
|
||||
# Get the text for a specific template present in local filesystem
|
||||
#
|
||||
# Parameters:
|
||||
# name (required) - The name of a template
|
||||
|
@ -23,13 +30,10 @@ module API
|
|||
# Example Request:
|
||||
# GET /gitignores/Elixir
|
||||
# GET /gitlab_ci_ymls/Ruby
|
||||
get "#{template}/:name" do
|
||||
get "#{template_type}/:name" do
|
||||
required_attributes! [:name]
|
||||
|
||||
new_template = klass.find(params[:name])
|
||||
not_found!(template.to_s.singularize) unless new_template
|
||||
|
||||
present new_template, with: Entities::Template
|
||||
render_response(template_type, new_template)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
module Gitlab
|
||||
module Template
|
||||
class BaseTemplate
|
||||
def initialize(path)
|
||||
def initialize(path, project = nil)
|
||||
@path = path
|
||||
@finder = self.class.finder(project)
|
||||
end
|
||||
|
||||
def name
|
||||
|
@ -10,23 +11,32 @@ module Gitlab
|
|||
end
|
||||
|
||||
def content
|
||||
File.read(@path)
|
||||
@finder.read(@path)
|
||||
end
|
||||
|
||||
def to_json
|
||||
{ name: name, content: content }
|
||||
end
|
||||
|
||||
class << self
|
||||
def all
|
||||
self.categories.keys.flat_map { |cat| by_category(cat) }
|
||||
def all(project = nil)
|
||||
if categories.any?
|
||||
categories.keys.flat_map { |cat| by_category(cat, project) }
|
||||
else
|
||||
by_category("", project)
|
||||
end
|
||||
end
|
||||
|
||||
def find(key)
|
||||
file_name = "#{key}#{self.extension}"
|
||||
|
||||
directory = select_directory(file_name)
|
||||
directory ? new(File.join(category_directory(directory), file_name)) : nil
|
||||
def find(key, project = nil)
|
||||
path = self.finder(project).find(key)
|
||||
path.present? ? new(path, project) : nil
|
||||
end
|
||||
|
||||
# Set categories as sub directories
|
||||
# Example: { "category_name_1" => "directory_path_1", "category_name_2" => "directory_name_2" }
|
||||
# Default is no category with all files in base dir of each class
|
||||
def categories
|
||||
raise NotImplementedError
|
||||
{}
|
||||
end
|
||||
|
||||
def extension
|
||||
|
@ -37,30 +47,41 @@ module Gitlab
|
|||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def by_category(category)
|
||||
templates_for_directory(category_directory(category))
|
||||
# Defines which strategy will be used to get templates files
|
||||
# RepoTemplateFinder - Finds templates on project repository, templates are filtered perproject
|
||||
# GlobalTemplateFinder - Finds templates on gitlab installation source, templates can be used in all projects
|
||||
def finder(project = nil)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def by_category(category, project = nil)
|
||||
directory = category_directory(category)
|
||||
files = finder(project).list_files_for(directory)
|
||||
|
||||
files.map { |f| new(f, project) }
|
||||
end
|
||||
|
||||
def category_directory(category)
|
||||
return base_dir unless category.present?
|
||||
|
||||
File.join(base_dir, categories[category])
|
||||
end
|
||||
|
||||
private
|
||||
# If template is organized by category it returns { category_name: [{ name: template_name }, { name: template2_name }] }
|
||||
# If no category is present returns [{ name: template_name }, { name: template2_name}]
|
||||
def dropdown_names(project = nil)
|
||||
return [] if project && !project.repository.exists?
|
||||
|
||||
def select_directory(file_name)
|
||||
categories.keys.find do |category|
|
||||
File.exist?(File.join(category_directory(category), file_name))
|
||||
if categories.any?
|
||||
categories.keys.map do |category|
|
||||
files = self.by_category(category, project)
|
||||
[category, files.map { |t| { name: t.name } }]
|
||||
end.to_h
|
||||
else
|
||||
files = self.all(project)
|
||||
files.map { |t| { name: t.name } }
|
||||
end
|
||||
end
|
||||
|
||||
def templates_for_directory(dir)
|
||||
dir << '/' unless dir.end_with?('/')
|
||||
Dir.glob(File.join(dir, "*#{self.extension}")).select { |f| f =~ filter_regex }.map { |f| new(f) }
|
||||
end
|
||||
|
||||
def filter_regex
|
||||
@filter_reges ||= /#{Regexp.escape(extension)}\z/
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
module Gitlab
|
||||
module Template
|
||||
module Finders
|
||||
class BaseTemplateFinder
|
||||
def initialize(base_dir)
|
||||
@base_dir = base_dir
|
||||
end
|
||||
|
||||
def list_files_for
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def read
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def find
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def category_directory(category)
|
||||
return @base_dir unless category.present?
|
||||
|
||||
@base_dir + @categories[category]
|
||||
end
|
||||
|
||||
class << self
|
||||
def filter_regex(extension)
|
||||
/#{Regexp.escape(extension)}\z/
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,38 @@
|
|||
# Searches and reads file present on Gitlab installation directory
|
||||
module Gitlab
|
||||
module Template
|
||||
module Finders
|
||||
class GlobalTemplateFinder < BaseTemplateFinder
|
||||
def initialize(base_dir, extension, categories = {})
|
||||
@categories = categories
|
||||
@extension = extension
|
||||
super(base_dir)
|
||||
end
|
||||
|
||||
def read(path)
|
||||
File.read(path)
|
||||
end
|
||||
|
||||
def find(key)
|
||||
file_name = "#{key}#{@extension}"
|
||||
|
||||
directory = select_directory(file_name)
|
||||
directory ? File.join(category_directory(directory), file_name) : nil
|
||||
end
|
||||
|
||||
def list_files_for(dir)
|
||||
dir << '/' unless dir.end_with?('/')
|
||||
Dir.glob(File.join(dir, "*#{@extension}")).select { |f| f =~ self.class.filter_regex(@extension) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def select_directory(file_name)
|
||||
@categories.keys.find do |category|
|
||||
File.exist?(File.join(category_directory(category), file_name))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,59 @@
|
|||
# Searches and reads files present on each Gitlab project repository
|
||||
module Gitlab
|
||||
module Template
|
||||
module Finders
|
||||
class RepoTemplateFinder < BaseTemplateFinder
|
||||
# Raised when file is not found
|
||||
class FileNotFoundError < StandardError; end
|
||||
|
||||
def initialize(project, base_dir, extension, categories = {})
|
||||
@categories = categories
|
||||
@extension = extension
|
||||
@repository = project.repository
|
||||
@commit = @repository.head_commit if @repository.exists?
|
||||
|
||||
super(base_dir)
|
||||
end
|
||||
|
||||
def read(path)
|
||||
blob = @repository.blob_at(@commit.id, path) if @commit
|
||||
raise FileNotFoundError if blob.nil?
|
||||
blob.data
|
||||
end
|
||||
|
||||
def find(key)
|
||||
file_name = "#{key}#{@extension}"
|
||||
directory = select_directory(file_name)
|
||||
raise FileNotFoundError if directory.nil?
|
||||
|
||||
category_directory(directory) + file_name
|
||||
end
|
||||
|
||||
def list_files_for(dir)
|
||||
return [] unless @commit
|
||||
|
||||
dir << '/' unless dir.end_with?('/')
|
||||
|
||||
entries = @repository.tree(:head, dir).entries
|
||||
|
||||
names = entries.map(&:name)
|
||||
names.select { |f| f =~ self.class.filter_regex(@extension) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def select_directory(file_name)
|
||||
return [] unless @commit
|
||||
|
||||
# Insert root as directory
|
||||
directories = ["", @categories.keys]
|
||||
|
||||
directories.find do |category|
|
||||
path = category_directory(category) + file_name
|
||||
@repository.blob_at(@commit.id, path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,6 +1,6 @@
|
|||
module Gitlab
|
||||
module Template
|
||||
class Gitignore < BaseTemplate
|
||||
class GitignoreTemplate < BaseTemplate
|
||||
class << self
|
||||
def extension
|
||||
'.gitignore'
|
||||
|
@ -16,6 +16,10 @@ module Gitlab
|
|||
def base_dir
|
||||
Rails.root.join('vendor/gitignore')
|
||||
end
|
||||
|
||||
def finder(project = nil)
|
||||
Gitlab::Template::Finders::GlobalTemplateFinder.new(self.base_dir, self.extension, self.categories)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,6 +1,6 @@
|
|||
module Gitlab
|
||||
module Template
|
||||
class GitlabCiYml < BaseTemplate
|
||||
class GitlabCiYmlTemplate < BaseTemplate
|
||||
def content
|
||||
explanation = "# This file is a template, and might need editing before it works on your project."
|
||||
[explanation, super].join("\n")
|
||||
|
@ -21,6 +21,10 @@ module Gitlab
|
|||
def base_dir
|
||||
Rails.root.join('vendor/gitlab-ci-yml')
|
||||
end
|
||||
|
||||
def finder(project = nil)
|
||||
Gitlab::Template::Finders::GlobalTemplateFinder.new(self.base_dir, self.extension, self.categories)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
module Gitlab
|
||||
module Template
|
||||
class IssueTemplate < BaseTemplate
|
||||
class << self
|
||||
def extension
|
||||
'.md'
|
||||
end
|
||||
|
||||
def base_dir
|
||||
'.gitlab/issue_templates/'
|
||||
end
|
||||
|
||||
def finder(project)
|
||||
Gitlab::Template::Finders::RepoTemplateFinder.new(project, self.base_dir, self.extension, self.categories)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
module Gitlab
|
||||
module Template
|
||||
class MergeRequestTemplate < BaseTemplate
|
||||
class << self
|
||||
def extension
|
||||
'.md'
|
||||
end
|
||||
|
||||
def base_dir
|
||||
'.gitlab/merge_request_templates/'
|
||||
end
|
||||
|
||||
def finder(project)
|
||||
Gitlab::Template::Finders::RepoTemplateFinder.new(project, self.base_dir, self.extension, self.categories)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,48 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Projects::TemplatesController do
|
||||
let(:project) { create(:project) }
|
||||
let(:user) { create(:user) }
|
||||
let(:user2) { create(:user) }
|
||||
let(:file_path_1) { '.gitlab/issue_templates/bug.md' }
|
||||
let(:body) { JSON.parse(response.body) }
|
||||
|
||||
before do
|
||||
project.team << [user, :developer]
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
before do
|
||||
project.team.add_user(user, Gitlab::Access::MASTER)
|
||||
project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
|
||||
end
|
||||
|
||||
describe '#show' do
|
||||
it 'renders template name and content as json' do
|
||||
get(:show, namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project.path, format: :json)
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(body["name"]).to eq("bug")
|
||||
expect(body["content"]).to eq("something valid")
|
||||
end
|
||||
|
||||
it 'renders 404 when unauthorized' do
|
||||
sign_in(user2)
|
||||
get(:show, namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project.path, format: :json)
|
||||
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
|
||||
it 'renders 404 when template type is not found' do
|
||||
sign_in(user)
|
||||
get(:show, namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project.path, format: :json)
|
||||
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
|
||||
it 'renders 404 without errors' do
|
||||
sign_in(user)
|
||||
expect { get(:show, namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project.path, format: :json) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,89 @@
|
|||
require 'spec_helper'
|
||||
|
||||
feature 'issuable templates', feature: true, js: true do
|
||||
include WaitForAjax
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project, :public) }
|
||||
|
||||
before do
|
||||
project.team << [user, :master]
|
||||
login_as user
|
||||
end
|
||||
|
||||
context 'user creates an issue using templates' do
|
||||
let(:template_content) { 'this is a test "bug" template' }
|
||||
let(:issue) { create(:issue, author: user, assignee: user, project: project) }
|
||||
|
||||
background do
|
||||
project.repository.commit_file(user, '.gitlab/issue_templates/bug.md', template_content, 'added issue template', 'master', false)
|
||||
visit edit_namespace_project_issue_path project.namespace, project, issue
|
||||
fill_in :'issue[title]', with: 'test issue title'
|
||||
end
|
||||
|
||||
scenario 'user selects "bug" template' do
|
||||
select_template 'bug'
|
||||
wait_for_ajax
|
||||
preview_template
|
||||
save_changes
|
||||
end
|
||||
end
|
||||
|
||||
context 'user creates a merge request using templates' do
|
||||
let(:template_content) { 'this is a test "feature-proposal" template' }
|
||||
let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) }
|
||||
|
||||
background do
|
||||
project.repository.commit_file(user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, 'added merge request template', 'master', false)
|
||||
visit edit_namespace_project_merge_request_path project.namespace, project, merge_request
|
||||
fill_in :'merge_request[title]', with: 'test merge request title'
|
||||
end
|
||||
|
||||
scenario 'user selects "feature-proposal" template' do
|
||||
select_template 'feature-proposal'
|
||||
wait_for_ajax
|
||||
preview_template
|
||||
save_changes
|
||||
end
|
||||
end
|
||||
|
||||
context 'user creates a merge request from a forked project using templates' do
|
||||
let(:template_content) { 'this is a test "feature-proposal" template' }
|
||||
let(:fork_user) { create(:user) }
|
||||
let(:fork_project) { create(:project, :public) }
|
||||
let(:merge_request) { create(:merge_request, :with_diffs, source_project: fork_project) }
|
||||
|
||||
background do
|
||||
logout
|
||||
project.team << [fork_user, :developer]
|
||||
fork_project.team << [fork_user, :master]
|
||||
create(:forked_project_link, forked_to_project: fork_project, forked_from_project: project)
|
||||
login_as fork_user
|
||||
fork_project.repository.commit_file(fork_user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, 'added merge request template', 'master', false)
|
||||
visit edit_namespace_project_merge_request_path fork_project.namespace, fork_project, merge_request
|
||||
fill_in :'merge_request[title]', with: 'test merge request title'
|
||||
end
|
||||
|
||||
scenario 'user selects "feature-proposal" template' do
|
||||
select_template 'feature-proposal'
|
||||
wait_for_ajax
|
||||
preview_template
|
||||
save_changes
|
||||
end
|
||||
end
|
||||
|
||||
def preview_template
|
||||
click_link 'Preview'
|
||||
expect(page).to have_content template_content
|
||||
end
|
||||
|
||||
def save_changes
|
||||
click_button "Save changes"
|
||||
expect(page).to have_content template_content
|
||||
end
|
||||
|
||||
def select_template(name)
|
||||
first('.js-issuable-selector').click
|
||||
first('.js-issuable-selector-wrap .dropdown-content a', text: name).click
|
||||
end
|
||||
end
|
|
@ -1,6 +1,6 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::Template::Gitignore do
|
||||
describe Gitlab::Template::GitignoreTemplate do
|
||||
subject { described_class }
|
||||
|
||||
describe '.all' do
|
||||
|
@ -24,7 +24,7 @@ describe Gitlab::Template::Gitignore do
|
|||
it 'returns the Gitignore object of a valid file' do
|
||||
ruby = subject.find('Ruby')
|
||||
|
||||
expect(ruby).to be_a Gitlab::Template::Gitignore
|
||||
expect(ruby).to be_a Gitlab::Template::GitignoreTemplate
|
||||
expect(ruby.name).to eq('Ruby')
|
||||
end
|
||||
end
|
|
@ -0,0 +1,41 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::Template::GitlabCiYmlTemplate do
|
||||
subject { described_class }
|
||||
|
||||
describe '.all' do
|
||||
it 'strips the gitlab-ci suffix' do
|
||||
expect(subject.all.first.name).not_to end_with('.gitlab-ci.yml')
|
||||
end
|
||||
|
||||
it 'combines the globals and rest' do
|
||||
all = subject.all.map(&:name)
|
||||
|
||||
expect(all).to include('Elixir')
|
||||
expect(all).to include('Docker')
|
||||
expect(all).to include('Ruby')
|
||||
end
|
||||
end
|
||||
|
||||
describe '.find' do
|
||||
it 'returns nil if the file does not exist' do
|
||||
expect(subject.find('mepmep-yadida')).to be nil
|
||||
end
|
||||
|
||||
it 'returns the GitlabCiYml object of a valid file' do
|
||||
ruby = subject.find('Ruby')
|
||||
|
||||
expect(ruby).to be_a Gitlab::Template::GitlabCiYmlTemplate
|
||||
expect(ruby.name).to eq('Ruby')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#content' do
|
||||
it 'loads the full file' do
|
||||
gitignore = subject.new(Rails.root.join('vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml'))
|
||||
|
||||
expect(gitignore.name).to eq 'Ruby'
|
||||
expect(gitignore.content).to start_with('#')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,89 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::Template::IssueTemplate do
|
||||
subject { described_class }
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project) }
|
||||
let(:file_path_1) { '.gitlab/issue_templates/bug.md' }
|
||||
let(:file_path_2) { '.gitlab/issue_templates/template_test.md' }
|
||||
let(:file_path_3) { '.gitlab/issue_templates/feature_proposal.md' }
|
||||
|
||||
before do
|
||||
project.team.add_user(user, Gitlab::Access::MASTER)
|
||||
project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
|
||||
project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false)
|
||||
project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false)
|
||||
end
|
||||
|
||||
describe '.all' do
|
||||
it 'strips the md suffix' do
|
||||
expect(subject.all(project).first.name).not_to end_with('.issue_template')
|
||||
end
|
||||
|
||||
it 'combines the globals and rest' do
|
||||
all = subject.all(project).map(&:name)
|
||||
|
||||
expect(all).to include('bug')
|
||||
expect(all).to include('feature_proposal')
|
||||
expect(all).to include('template_test')
|
||||
end
|
||||
end
|
||||
|
||||
describe '.find' do
|
||||
it 'returns nil if the file does not exist' do
|
||||
expect { subject.find('mepmep-yadida', project) }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
|
||||
end
|
||||
|
||||
it 'returns the issue object of a valid file' do
|
||||
ruby = subject.find('bug', project)
|
||||
|
||||
expect(ruby).to be_a Gitlab::Template::IssueTemplate
|
||||
expect(ruby.name).to eq('bug')
|
||||
end
|
||||
end
|
||||
|
||||
describe '.by_category' do
|
||||
it 'return array of templates' do
|
||||
all = subject.by_category('', project).map(&:name)
|
||||
expect(all).to include('bug')
|
||||
expect(all).to include('feature_proposal')
|
||||
expect(all).to include('template_test')
|
||||
end
|
||||
|
||||
context 'when repo is bare or empty' do
|
||||
let(:empty_project) { create(:empty_project) }
|
||||
before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
|
||||
|
||||
it "returns empty array" do
|
||||
templates = subject.by_category('', empty_project)
|
||||
expect(templates).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#content' do
|
||||
it 'loads the full file' do
|
||||
issue_template = subject.new('.gitlab/issue_templates/bug.md', project)
|
||||
|
||||
expect(issue_template.name).to eq 'bug'
|
||||
expect(issue_template.content).to eq('something valid')
|
||||
end
|
||||
|
||||
it 'raises error when file is not found' do
|
||||
issue_template = subject.new('.gitlab/issue_templates/bugnot.md', project)
|
||||
expect { issue_template.content }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
|
||||
end
|
||||
|
||||
context "when repo is empty" do
|
||||
let(:empty_project) { create(:empty_project) }
|
||||
|
||||
before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
|
||||
|
||||
it "raises file not found" do
|
||||
issue_template = subject.new('.gitlab/issue_templates/not_existent.md', empty_project)
|
||||
expect { issue_template.content }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,89 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::Template::MergeRequestTemplate do
|
||||
subject { described_class }
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project) }
|
||||
let(:file_path_1) { '.gitlab/merge_request_templates/bug.md' }
|
||||
let(:file_path_2) { '.gitlab/merge_request_templates/template_test.md' }
|
||||
let(:file_path_3) { '.gitlab/merge_request_templates/feature_proposal.md' }
|
||||
|
||||
before do
|
||||
project.team.add_user(user, Gitlab::Access::MASTER)
|
||||
project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
|
||||
project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false)
|
||||
project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false)
|
||||
end
|
||||
|
||||
describe '.all' do
|
||||
it 'strips the md suffix' do
|
||||
expect(subject.all(project).first.name).not_to end_with('.issue_template')
|
||||
end
|
||||
|
||||
it 'combines the globals and rest' do
|
||||
all = subject.all(project).map(&:name)
|
||||
|
||||
expect(all).to include('bug')
|
||||
expect(all).to include('feature_proposal')
|
||||
expect(all).to include('template_test')
|
||||
end
|
||||
end
|
||||
|
||||
describe '.find' do
|
||||
it 'returns nil if the file does not exist' do
|
||||
expect { subject.find('mepmep-yadida', project) }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
|
||||
end
|
||||
|
||||
it 'returns the merge request object of a valid file' do
|
||||
ruby = subject.find('bug', project)
|
||||
|
||||
expect(ruby).to be_a Gitlab::Template::MergeRequestTemplate
|
||||
expect(ruby.name).to eq('bug')
|
||||
end
|
||||
end
|
||||
|
||||
describe '.by_category' do
|
||||
it 'return array of templates' do
|
||||
all = subject.by_category('', project).map(&:name)
|
||||
expect(all).to include('bug')
|
||||
expect(all).to include('feature_proposal')
|
||||
expect(all).to include('template_test')
|
||||
end
|
||||
|
||||
context 'when repo is bare or empty' do
|
||||
let(:empty_project) { create(:empty_project) }
|
||||
before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
|
||||
|
||||
it "returns empty array" do
|
||||
templates = subject.by_category('', empty_project)
|
||||
expect(templates).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#content' do
|
||||
it 'loads the full file' do
|
||||
issue_template = subject.new('.gitlab/merge_request_templates/bug.md', project)
|
||||
|
||||
expect(issue_template.name).to eq 'bug'
|
||||
expect(issue_template.content).to eq('something valid')
|
||||
end
|
||||
|
||||
it 'raises error when file is not found' do
|
||||
issue_template = subject.new('.gitlab/merge_request_templates/bugnot.md', project)
|
||||
expect { issue_template.content }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
|
||||
end
|
||||
|
||||
context "when repo is empty" do
|
||||
let(:empty_project) { create(:empty_project) }
|
||||
|
||||
before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
|
||||
|
||||
it "raises file not found" do
|
||||
issue_template = subject.new('.gitlab/merge_request_templates/not_existent.md', empty_project)
|
||||
expect { issue_template.content }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,50 +3,53 @@ require 'spec_helper'
|
|||
describe API::Templates, api: true do
|
||||
include ApiHelpers
|
||||
|
||||
describe 'the Template Entity' do
|
||||
before { get api('/gitignores/Ruby') }
|
||||
context 'global templates' do
|
||||
describe 'the Template Entity' do
|
||||
before { get api('/gitignores/Ruby') }
|
||||
|
||||
it { expect(json_response['name']).to eq('Ruby') }
|
||||
it { expect(json_response['content']).to include('*.gem') }
|
||||
end
|
||||
it { expect(json_response['name']).to eq('Ruby') }
|
||||
it { expect(json_response['content']).to include('*.gem') }
|
||||
end
|
||||
|
||||
describe 'the TemplateList Entity' do
|
||||
before { get api('/gitignores') }
|
||||
describe 'the TemplateList Entity' do
|
||||
before { get api('/gitignores') }
|
||||
|
||||
it { expect(json_response.first['name']).not_to be_nil }
|
||||
it { expect(json_response.first['content']).to be_nil }
|
||||
end
|
||||
it { expect(json_response.first['name']).not_to be_nil }
|
||||
it { expect(json_response.first['content']).to be_nil }
|
||||
end
|
||||
|
||||
context 'requesting gitignores' do
|
||||
describe 'GET /gitignores' do
|
||||
it 'returns a list of available gitignore templates' do
|
||||
get api('/gitignores')
|
||||
context 'requesting gitignores' do
|
||||
describe 'GET /gitignores' do
|
||||
it 'returns a list of available gitignore templates' do
|
||||
get api('/gitignores')
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response).to be_an Array
|
||||
expect(json_response.size).to be > 15
|
||||
expect(response.status).to eq(200)
|
||||
expect(json_response).to be_an Array
|
||||
expect(json_response.size).to be > 15
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'requesting gitlab-ci-ymls' do
|
||||
describe 'GET /gitlab_ci_ymls' do
|
||||
it 'returns a list of available gitlab_ci_ymls' do
|
||||
get api('/gitlab_ci_ymls')
|
||||
context 'requesting gitlab-ci-ymls' do
|
||||
describe 'GET /gitlab_ci_ymls' do
|
||||
it 'returns a list of available gitlab_ci_ymls' do
|
||||
get api('/gitlab_ci_ymls')
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response).to be_an Array
|
||||
expect(json_response.first['name']).not_to be_nil
|
||||
expect(response.status).to eq(200)
|
||||
expect(json_response).to be_an Array
|
||||
expect(json_response.first['name']).not_to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /gitlab_ci_ymls/Ruby' do
|
||||
it 'adds a disclaimer on the top' do
|
||||
get api('/gitlab_ci_ymls/Ruby')
|
||||
describe 'GET /gitlab_ci_ymls/Ruby' do
|
||||
it 'adds a disclaimer on the top' do
|
||||
get api('/gitlab_ci_ymls/Ruby')
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response['content']).to start_with("# This file is a template,")
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response['name']).not_to be_nil
|
||||
expect(json_response['content']).to start_with("# This file is a template,")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue