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 9299639a3ab3..aececed9adde 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 49c2ac0dac3f..84b292e59c64 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 f1aab067351d..e596b98603b6 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 2cf0a6631b8d..b0a37ef0e0a4 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 3946e8619766..7160fa71ce56 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 000000000000..c32ddf802199 --- /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 000000000000..bd8cdde033ef --- /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 e8eafa158990..f1635a537630 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 7a50bc9c8320..46c4a11aa2eb 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 000000000000..694b468c8d37 --- /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 48c278282190..1cb5d8476266 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -182,17 +182,42 @@ def licenses_for_select } 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 0b7fa8c7d06d..c957cd84479f 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 c30bdb0ae913..210b43c7e0b5 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 1d2db91344f2..63a8827a6a23 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -528,6 +528,11 @@ 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 49dec6137165..993349e5b46f 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 000000000000..9514564af023 --- /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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%0DIeEoa6}C!X6;^r0 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 literal 0 HcmV?d00001 diff --git a/lib/api/templates.rb b/lib/api/templates.rb index 184087977566..b9e718147e10 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 @@ class Templates < Grape::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 760ff3e614a6..7ebec8e2cff2 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 @@ def name 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 @@ def base_dir 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 000000000000..473b05257c62 --- /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 000000000000..831da45191f8 --- /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 000000000000..22c39436cb24 --- /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 964fbfd4de33..8d2a9d2305ca 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 @@ def categories 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 7f480fe33c0f..8d1a1ed54c9d 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 @@ def categories 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 000000000000..c6fa8d3eafcc --- /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 000000000000..f826c02f3b53 --- /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 000000000000..7b3a26d7ca77 --- /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 000000000000..4a83740621a6 --- /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 bc0ec9325cc1..9750a012e22d 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 @@ 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 000000000000..e3b8321eda39 --- /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 000000000000..f770857e9588 --- /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 000000000000..bb0f68043fa0 --- /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 68d0f41b489b..5bd5b861792d 100644 --- a/spec/requests/api/templates_spec.rb +++ b/spec/requests/api/templates_spec.rb @@ -3,50 +3,53 @@ 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 -- GitLab