From 28726729452ef64270534806e75a9595ea1a659d Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Fri, 24 Jun 2016 16:43:46 -0300 Subject: [PATCH] Load issues and merge requests templates from repository --- CHANGELOG | 1 + app/assets/javascripts/api.js | 51 +++++----- app/assets/javascripts/application.js | 1 + .../javascripts/blob/template_selector.js | 22 ++++- app/assets/javascripts/dispatcher.js | 2 + .../issuable_template_selector.js.es6 | 51 ++++++++++ .../issuable_template_selectors.js.es6 | 29 ++++++ .../stylesheets/framework/dropdowns.scss | 7 +- app/assets/stylesheets/pages/issuable.scss | 9 ++ .../projects/templates_controller.rb | 19 ++++ app/helpers/blob_helper.rb | 41 ++++++-- app/views/shared/issuable/_filter.html.haml | 2 +- app/views/shared/issuable/_form.html.haml | 24 ++++- config/routes.rb | 5 + doc/workflow/README.md | 1 + doc/workflow/description_templates.md | 12 +++ doc/workflow/img/description_templates.png | Bin 0 -> 57670 bytes lib/api/templates.rb | 26 ++--- lib/gitlab/template/base_template.rb | 71 +++++++++----- .../template/finders/base_template_finder.rb | 35 +++++++ .../finders/global_template_finder.rb | 38 ++++++++ .../template/finders/repo_template_finder.rb | 59 ++++++++++++ .../{gitignore.rb => gitignore_template.rb} | 6 +- ...ab_ci_yml.rb => gitlab_ci_yml_template.rb} | 6 +- lib/gitlab/template/issue_template.rb | 19 ++++ lib/gitlab/template/merge_request_template.rb | 19 ++++ .../projects/templates_controller_spec.rb | 48 ++++++++++ .../projects/issuable_templates_spec.rb | 89 ++++++++++++++++++ ...ore_spec.rb => gitignore_template_spec.rb} | 4 +- .../template/gitlab_ci_yml_template_spec.rb | 41 ++++++++ .../gitlab/template/issue_template_spec.rb | 89 ++++++++++++++++++ .../template/merge_request_template_spec.rb | 89 ++++++++++++++++++ spec/requests/api/templates_spec.rb | 65 +++++++------ 33 files changed, 875 insertions(+), 106 deletions(-) create mode 100644 app/assets/javascripts/templates/issuable_template_selector.js.es6 create mode 100644 app/assets/javascripts/templates/issuable_template_selectors.js.es6 create mode 100644 app/controllers/projects/templates_controller.rb create mode 100644 doc/workflow/description_templates.md create mode 100644 doc/workflow/img/description_templates.png create mode 100644 lib/gitlab/template/finders/base_template_finder.rb create mode 100644 lib/gitlab/template/finders/global_template_finder.rb create mode 100644 lib/gitlab/template/finders/repo_template_finder.rb rename lib/gitlab/template/{gitignore.rb => gitignore_template.rb} (63%) rename lib/gitlab/template/{gitlab_ci_yml.rb => gitlab_ci_yml_template.rb} (72%) create mode 100644 lib/gitlab/template/issue_template.rb create mode 100644 lib/gitlab/template/merge_request_template.rb create mode 100644 spec/controllers/projects/templates_controller_spec.rb create mode 100644 spec/features/projects/issuable_templates_spec.rb rename spec/lib/gitlab/template/{gitignore_spec.rb => gitignore_template_spec.rb} (88%) create mode 100644 spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb create mode 100644 spec/lib/gitlab/template/issue_template_spec.rb create mode 100644 spec/lib/gitlab/template/merge_request_template_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 9299639a3ab..aececed9add 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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) diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 49c2ac0dac3..84b292e59c6 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -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; diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index f1aab067351..e596b98603b 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -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 */ diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js index 2cf0a6631b8..b0a37ef0e0a 100644 --- a/app/assets/javascripts/blob/template_selector.js +++ b/app/assets/javascripts/blob/template_selector.js @@ -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; diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 3946e861976..7160fa71ce5 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -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(); diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6 new file mode 100644 index 00000000000..c32ddf80219 --- /dev/null +++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6 @@ -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); diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 b/app/assets/javascripts/templates/issuable_template_selectors.js.es6 new file mode 100644 index 00000000000..bd8cdde033e --- /dev/null +++ b/app/assets/javascripts/templates/issuable_template_selectors.js.es6 @@ -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); diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index e8eafa15899..f1635a53763 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -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; } } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 7a50bc9c832..46c4a11aa2e 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -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; + } +} diff --git a/app/controllers/projects/templates_controller.rb b/app/controllers/projects/templates_controller.rb new file mode 100644 index 00000000000..694b468c8d3 --- /dev/null +++ b/app/controllers/projects/templates_controller.rb @@ -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 diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 48c27828219..1cb5d847626 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -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 diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 0b7fa8c7d06..c957cd84479 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -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 diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index c30bdb0ae91..210b43c7e0b 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -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 diff --git a/config/routes.rb b/config/routes.rb index 1d2db91344f..63a8827a6a2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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', diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 49dec613716..993349e5b46 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -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) diff --git a/doc/workflow/description_templates.md b/doc/workflow/description_templates.md new file mode 100644 index 00000000000..9514564af02 --- /dev/null +++ b/doc/workflow/description_templates.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. diff --git a/doc/workflow/img/description_templates.png b/doc/workflow/img/description_templates.png new file mode 100644 index 0000000000000000000000000000000000000000..af2e9403826121a061c882ff509c8ba5653673b7 GIT binary patch literal 57670 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 literal 0 HcmV?d00001 diff --git a/lib/api/templates.rb b/lib/api/templates.rb index 18408797756..b9e718147e1 100644 --- a/lib/api/templates.rb +++ b/lib/api/templates.rb @@ -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 diff --git a/lib/gitlab/template/base_template.rb b/lib/gitlab/template/base_template.rb index 760ff3e614a..7ebec8e2cff 100644 --- a/lib/gitlab/template/base_template.rb +++ b/lib/gitlab/template/base_template.rb @@ -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,29 +47,40 @@ 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 category_directory(category) - File.join(base_dir, categories[category]) + def by_category(category, project = nil) + directory = category_directory(category) + files = finder(project).list_files_for(directory) + + files.map { |f| new(f, project) } end - private + def category_directory(category) + return base_dir unless category.present? - def select_directory(file_name) - categories.keys.find do |category| - File.exist?(File.join(category_directory(category), file_name)) - end + File.join(base_dir, categories[category]) 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 + # 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 filter_regex - @filter_reges ||= /#{Regexp.escape(extension)}\z/ + 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 end end diff --git a/lib/gitlab/template/finders/base_template_finder.rb b/lib/gitlab/template/finders/base_template_finder.rb new file mode 100644 index 00000000000..473b05257c6 --- /dev/null +++ b/lib/gitlab/template/finders/base_template_finder.rb @@ -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 diff --git a/lib/gitlab/template/finders/global_template_finder.rb b/lib/gitlab/template/finders/global_template_finder.rb new file mode 100644 index 00000000000..831da45191f --- /dev/null +++ b/lib/gitlab/template/finders/global_template_finder.rb @@ -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 diff --git a/lib/gitlab/template/finders/repo_template_finder.rb b/lib/gitlab/template/finders/repo_template_finder.rb new file mode 100644 index 00000000000..22c39436cb2 --- /dev/null +++ b/lib/gitlab/template/finders/repo_template_finder.rb @@ -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 diff --git a/lib/gitlab/template/gitignore.rb b/lib/gitlab/template/gitignore_template.rb similarity index 63% rename from lib/gitlab/template/gitignore.rb rename to lib/gitlab/template/gitignore_template.rb index 964fbfd4de3..8d2a9d2305c 100644 --- a/lib/gitlab/template/gitignore.rb +++ b/lib/gitlab/template/gitignore_template.rb @@ -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 diff --git a/lib/gitlab/template/gitlab_ci_yml.rb b/lib/gitlab/template/gitlab_ci_yml_template.rb similarity index 72% rename from lib/gitlab/template/gitlab_ci_yml.rb rename to lib/gitlab/template/gitlab_ci_yml_template.rb index 7f480fe33c0..8d1a1ed54c9 100644 --- a/lib/gitlab/template/gitlab_ci_yml.rb +++ b/lib/gitlab/template/gitlab_ci_yml_template.rb @@ -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 diff --git a/lib/gitlab/template/issue_template.rb b/lib/gitlab/template/issue_template.rb new file mode 100644 index 00000000000..c6fa8d3eafc --- /dev/null +++ b/lib/gitlab/template/issue_template.rb @@ -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 diff --git a/lib/gitlab/template/merge_request_template.rb b/lib/gitlab/template/merge_request_template.rb new file mode 100644 index 00000000000..f826c02f3b5 --- /dev/null +++ b/lib/gitlab/template/merge_request_template.rb @@ -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 diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb new file mode 100644 index 00000000000..7b3a26d7ca7 --- /dev/null +++ b/spec/controllers/projects/templates_controller_spec.rb @@ -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 diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb new file mode 100644 index 00000000000..4a83740621a --- /dev/null +++ b/spec/features/projects/issuable_templates_spec.rb @@ -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 diff --git a/spec/lib/gitlab/template/gitignore_spec.rb b/spec/lib/gitlab/template/gitignore_template_spec.rb similarity index 88% rename from spec/lib/gitlab/template/gitignore_spec.rb rename to spec/lib/gitlab/template/gitignore_template_spec.rb index bc0ec9325cc..9750a012e22 100644 --- a/spec/lib/gitlab/template/gitignore_spec.rb +++ b/spec/lib/gitlab/template/gitignore_template_spec.rb @@ -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 diff --git a/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb new file mode 100644 index 00000000000..e3b8321eda3 --- /dev/null +++ b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb @@ -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 diff --git a/spec/lib/gitlab/template/issue_template_spec.rb b/spec/lib/gitlab/template/issue_template_spec.rb new file mode 100644 index 00000000000..f770857e958 --- /dev/null +++ b/spec/lib/gitlab/template/issue_template_spec.rb @@ -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 diff --git a/spec/lib/gitlab/template/merge_request_template_spec.rb b/spec/lib/gitlab/template/merge_request_template_spec.rb new file mode 100644 index 00000000000..bb0f68043fa --- /dev/null +++ b/spec/lib/gitlab/template/merge_request_template_spec.rb @@ -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 diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb index 68d0f41b489..5bd5b861792 100644 --- a/spec/requests/api/templates_spec.rb +++ b/spec/requests/api/templates_spec.rb @@ -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 -- 2.18.1