diff options
author | Nick Thomas <nick@gitlab.com> | 2018-10-03 02:00:38 +0300 |
---|---|---|
committer | Nick Thomas <nick@gitlab.com> | 2018-10-05 13:34:43 +0300 |
commit | 25bd49e4f57fe15f9d61dc9376a5b7dc35b30f64 (patch) | |
tree | faef4e9d73e9845413462013c868eace19a11abf | |
parent | ae014e189773f7299c12c1050334b3e8fe7b15d8 (diff) |
Backport project template API to CE
33 files changed, 756 insertions, 705 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index ecc2440c7e6..3f7a1ef1bfc 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -15,12 +15,9 @@ const Api = { mergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes', mergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions', groupLabelsPath: '/groups/:namespace_path/-/labels', - templatesPath: '/api/:version/templates/:key', - licensePath: '/api/:version/templates/licenses/:key', - gitignorePath: '/api/:version/templates/gitignores/:key', - gitlabCiYmlPath: '/api/:version/templates/gitlab_ci_ymls/:key', - dockerfilePath: '/api/:version/templates/dockerfiles/:key', issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key', + projectTemplatePath: '/api/:version/projects/:id/templates/:type/:key', + projectTemplatesPath: '/api/:version/projects/:id/templates/:type', usersPath: '/api/:version/users.json', userStatusPath: '/api/:version/user/status', commitPath: '/api/:version/projects/:id/repository/commits', @@ -196,29 +193,29 @@ const Api = { return axios.get(url); }, - // Return text for a specific license - licenseText(key, data, callback) { - const url = Api.buildUrl(Api.licensePath).replace(':key', key); - return axios - .get(url, { - params: data, - }) - .then(res => callback(res.data)); - }, + projectTemplate(id, type, key, options, callback) { + const url = Api.buildUrl(this.projectTemplatePath) + .replace(':id', encodeURIComponent(id)) + .replace(':type', type) + .replace(':key', encodeURIComponent(key)); - gitignoreText(key, callback) { - const url = Api.buildUrl(Api.gitignorePath).replace(':key', key); - return axios.get(url).then(({ data }) => callback(data)); - }, + return axios.get(url, { params: options }).then(res => { + if (callback) callback(res.data); - gitlabCiYml(key, callback) { - const url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key); - return axios.get(url).then(({ data }) => callback(data)); + return res; + }); }, - dockerfileYml(key, callback) { - const url = Api.buildUrl(Api.dockerfilePath).replace(':key', key); - return axios.get(url).then(({ data }) => callback(data)); + projectTemplates(id, type, params = {}, callback) { + const url = Api.buildUrl(this.projectTemplatesPath) + .replace(':id', encodeURIComponent(id)) + .replace(':type', type); + + return axios.get(url, { params }).then(res => { + if (callback) callback(res.data); + + return res; + }); }, issueTemplate(namespacePath, projectPath, key, type, callback) { @@ -276,12 +273,6 @@ const Api = { }); }, - templates(key, params = {}) { - const url = Api.buildUrl(this.templatesPath).replace(':key', key); - - return axios.get(url, { params }); - }, - buildUrl(url) { let urlRoot = ''; if (gon.relative_url_root != null) { diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js index ff1cbcad145..addacf29f1e 100644 --- a/app/assets/javascripts/blob/file_template_mediator.js +++ b/app/assets/javascripts/blob/file_template_mediator.js @@ -1,4 +1,4 @@ -/* eslint-disable class-methods-use-this */ +import Api from '~/api'; import $ from 'jquery'; import Flash from '../flash'; @@ -9,9 +9,10 @@ import GitignoreSelector from './template_selectors/gitignore_selector'; import LicenseSelector from './template_selectors/license_selector'; export default class FileTemplateMediator { - constructor({ editor, currentAction }) { + constructor({ editor, currentAction, projectId }) { this.editor = editor; this.currentAction = currentAction; + this.projectId = projectId; this.initTemplateSelectors(); this.initTemplateTypeSelector(); @@ -33,15 +34,14 @@ export default class FileTemplateMediator { initTemplateTypeSelector() { this.typeSelector = new FileTemplateTypeSelector({ mediator: this, - dropdownData: this.templateSelectors - .map((templateSelector) => { - const cfg = templateSelector.config; - - return { - name: cfg.name, - key: cfg.key, - }; - }), + dropdownData: this.templateSelectors.map(templateSelector => { + const cfg = templateSelector.config; + + return { + name: cfg.name, + key: cfg.key, + }; + }), }); } @@ -89,7 +89,7 @@ export default class FileTemplateMediator { } listenForPreviewMode() { - this.$navLinks.on('click', 'a', (e) => { + this.$navLinks.on('click', 'a', e => { const urlPieces = e.target.href.split('#'); const hash = urlPieces[1]; if (hash === 'preview') { @@ -105,7 +105,7 @@ export default class FileTemplateMediator { e.preventDefault(); } - this.templateSelectors.forEach((selector) => { + this.templateSelectors.forEach(selector => { if (selector.config.key === item.key) { selector.show(); } else { @@ -126,8 +126,8 @@ export default class FileTemplateMediator { selector.renderLoading(); // in case undo menu is already already there this.destroyUndoMenu(); - this.fetchFileTemplate(selector.config.endpoint, query, data) - .then((file) => { + this.fetchFileTemplate(selector.config.type, query, data) + .then(file => { this.showUndoMenu(); this.setEditorContent(file); this.setFilename(selector.config.name); @@ -138,7 +138,7 @@ export default class FileTemplateMediator { displayMatchedTemplateSelector() { const currentInput = this.getFilename(); - this.templateSelectors.forEach((selector) => { + this.templateSelectors.forEach(selector => { const match = selector.config.pattern.test(currentInput); if (match) { @@ -149,15 +149,11 @@ export default class FileTemplateMediator { }); } - fetchFileTemplate(apiCall, query, data) { - return new Promise((resolve) => { + fetchFileTemplate(type, query, data = {}) { + return new Promise(resolve => { const resolveFile = file => resolve(file); - if (!data) { - apiCall(query, resolveFile); - } else { - apiCall(query, data, resolveFile); - } + Api.projectTemplate(this.projectId, type, query, data, resolveFile); }); } diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js index 9dfdb06007d..9db1fa70ffb 100644 --- a/app/assets/javascripts/blob/template_selector.js +++ b/app/assets/javascripts/blob/template_selector.js @@ -66,9 +66,6 @@ export default class TemplateSelector { // be added by all subclasses. } - // To be implemented on the extending class - // e.g. Api.gitlabCiYml(query.name, file => this.setEditorContent(file)); - setEditorContent(file, { skipFocus } = {}) { if (!file) return; diff --git a/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js index 9c41e429c8d..43f7aead8b9 100644 --- a/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js +++ b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js @@ -1,5 +1,3 @@ -import Api from '../../api'; - import FileTemplateSelector from '../file_template_selector'; export default class BlobCiYamlSelector extends FileTemplateSelector { @@ -9,7 +7,7 @@ export default class BlobCiYamlSelector extends FileTemplateSelector { key: 'gitlab-ci-yaml', name: '.gitlab-ci.yml', pattern: /(.gitlab-ci.yml)/, - endpoint: Api.gitlabCiYml, + type: 'gitlab_ci_ymls', dropdown: '.js-gitlab-ci-yml-selector', wrapper: '.js-gitlab-ci-yml-selector-wrap', }; diff --git a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js index 45fb614fe00..4718b642617 100644 --- a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js +++ b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js @@ -1,5 +1,3 @@ -import Api from '../../api'; - import FileTemplateSelector from '../file_template_selector'; export default class DockerfileSelector extends FileTemplateSelector { @@ -9,7 +7,7 @@ export default class DockerfileSelector extends FileTemplateSelector { key: 'dockerfile', name: 'Dockerfile', pattern: /(Dockerfile)/, - endpoint: Api.dockerfileYml, + type: 'dockerfiles', dropdown: '.js-dockerfile-selector', wrapper: '.js-dockerfile-selector-wrap', }; diff --git a/app/assets/javascripts/blob/template_selectors/gitignore_selector.js b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js index a894953cc86..a8067ec5c84 100644 --- a/app/assets/javascripts/blob/template_selectors/gitignore_selector.js +++ b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js @@ -1,5 +1,3 @@ -import Api from '../../api'; - import FileTemplateSelector from '../file_template_selector'; export default class BlobGitignoreSelector extends FileTemplateSelector { @@ -9,7 +7,7 @@ export default class BlobGitignoreSelector extends FileTemplateSelector { key: 'gitignore', name: '.gitignore', pattern: /(.gitignore)/, - endpoint: Api.gitignoreText, + type: 'gitignores', dropdown: '.js-gitignore-selector', wrapper: '.js-gitignore-selector-wrap', }; diff --git a/app/assets/javascripts/blob/template_selectors/license_selector.js b/app/assets/javascripts/blob/template_selectors/license_selector.js index b7c4da0f62e..ac1fe95eee5 100644 --- a/app/assets/javascripts/blob/template_selectors/license_selector.js +++ b/app/assets/javascripts/blob/template_selectors/license_selector.js @@ -1,5 +1,3 @@ -import Api from '../../api'; - import FileTemplateSelector from '../file_template_selector'; export default class BlobLicenseSelector extends FileTemplateSelector { @@ -9,7 +7,7 @@ export default class BlobLicenseSelector extends FileTemplateSelector { key: 'license', name: 'LICENSE', pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i, - endpoint: Api.licenseText, + type: 'licenses', dropdown: '.js-license-selector', wrapper: '.js-license-selector-wrap', }; diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js index a603d89b84a..4e4598870fa 100644 --- a/app/assets/javascripts/blob_edit/blob_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_bundle.js @@ -15,8 +15,9 @@ export default () => { const assetsPath = editBlobForm.data('assetsPrefix'); const blobLanguage = editBlobForm.data('blobLanguage'); const currentAction = $('.js-file-title').data('currentAction'); + const projectId = editBlobForm.data('project-id'); - new EditBlob(`${urlRoot}${assetsPath}`, blobLanguage, currentAction); + new EditBlob(`${urlRoot}${assetsPath}`, blobLanguage, currentAction, projectId); new NewCommitForm(editBlobForm); } diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index 82a3d494b67..ec2b130ab7d 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -7,11 +7,11 @@ import { __ } from '~/locale'; import TemplateSelectorMediator from '../blob/file_template_mediator'; export default class EditBlob { - constructor(assetsPath, aceMode, currentAction) { + constructor(assetsPath, aceMode, currentAction, projectId) { this.configureAceEditor(aceMode, assetsPath); this.initModePanesAndLinks(); this.initSoftWrap(); - this.initFileSelectors(currentAction); + this.initFileSelectors(currentAction, projectId); } configureAceEditor(aceMode, assetsPath) { @@ -30,10 +30,11 @@ export default class EditBlob { } } - initFileSelectors(currentAction) { + initFileSelectors(currentAction, projectId) { this.fileTemplateMediator = new TemplateSelectorMediator({ currentAction, editor: this.editor, + projectId, }); } @@ -60,14 +61,15 @@ export default class EditBlob { if (paneId === '#preview') { this.$toggleButton.hide(); - axios.post(currentLink.data('previewUrl'), { - content: this.editor.getValue(), - }) - .then(({ data }) => { - currentPane.empty().append(data); - currentPane.renderGFM(); - }) - .catch(() => createFlash(__('An error occurred previewing the blob'))); + axios + .post(currentLink.data('previewUrl'), { + content: this.editor.getValue(), + }) + .then(({ data }) => { + currentPane.empty().append(data); + currentPane.renderGFM(); + }) + .catch(() => createFlash(__('An error occurred previewing the blob'))); } this.$toggleButton.show(); diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/actions.js b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js index cc9f6c8638c..b7090e09daf 100644 --- a/app/assets/javascripts/ide/stores/modules/file_templates/actions.js +++ b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js @@ -23,12 +23,12 @@ export const receiveTemplateTypesError = ({ commit, dispatch }) => { export const receiveTemplateTypesSuccess = ({ commit }, templates) => commit(types.RECEIVE_TEMPLATE_TYPES_SUCCESS, templates); -export const fetchTemplateTypes = ({ dispatch, state }, page = 1) => { +export const fetchTemplateTypes = ({ dispatch, state, rootState }, page = 1) => { if (!Object.keys(state.selectedTemplateType).length) return Promise.reject(); dispatch('requestTemplateTypes'); - return Api.templates(state.selectedTemplateType.key, { page }) + return Api.projectTemplates(rootState.currentProjectId, state.selectedTemplateType.key, { page }) .then(({ data, headers }) => { const nextPage = parseInt(normalizeHeaders(headers)['X-NEXT-PAGE'], 10); @@ -74,12 +74,16 @@ export const receiveTemplateError = ({ dispatch }, template) => { ); }; -export const fetchTemplate = ({ dispatch, state }, template) => { +export const fetchTemplate = ({ dispatch, state, rootState }, template) => { if (template.content) { return dispatch('setFileTemplate', template); } - return Api.templates(`${state.selectedTemplateType.key}/${template.key || template.name}`) + return Api.projectTemplate( + rootState.currentProjectId, + state.selectedTemplateType.key, + template.key || template.name, + ) .then(({ data }) => { dispatch('setFileTemplate', data); }) diff --git a/app/finders/license_template_finder.rb b/app/finders/license_template_finder.rb index 196922709f7..d735a4c1d69 100644 --- a/app/finders/license_template_finder.rb +++ b/app/finders/license_template_finder.rb @@ -5,33 +5,47 @@ # Used to find license templates, which may come from a variety of external # sources # -# Arguments: +# Params can be any of the following: # popular: boolean. When set to true, only "popular" licenses are shown. When # false, all licenses except popular ones are shown. When nil (the # default), *all* licenses will be shown. +# name: string. If set, return a single license matching that name (or nil) class LicenseTemplateFinder - attr_reader :params + include Gitlab::Utils::StrongMemoize - def initialize(params = {}) + attr_reader :project, :params + + def initialize(project, params = {}) + @project = project @params = params end def execute - Licensee::License.all(featured: popular_only?).map do |license| - LicenseTemplate.new( - id: license.key, - name: license.name, - nickname: license.nickname, - category: (license.featured? ? :Popular : :Other), - content: license.content, - url: license.url, - meta: license.meta - ) + if params[:name] + vendored_licenses.find { |template| template.key == params[:name] } + else + vendored_licenses end end private + def vendored_licenses + strong_memoize(:vendored_licenses) do + Licensee::License.all(featured: popular_only?).map do |license| + LicenseTemplate.new( + key: license.key, + name: license.name, + nickname: license.nickname, + category: (license.featured? ? :Popular : :Other), + content: license.content, + url: license.url, + meta: license.meta + ) + end + end + end + def popular_only? params.fetch(:popular, nil) end diff --git a/app/finders/template_finder.rb b/app/finders/template_finder.rb index c92ee9ca9ac..3e483716064 100644 --- a/app/finders/template_finder.rb +++ b/app/finders/template_finder.rb @@ -1,29 +1,32 @@ # frozen_string_literal: true class TemplateFinder - VENDORED_TEMPLATES = { + include Gitlab::Utils::StrongMemoize + + VENDORED_TEMPLATES = HashWithIndifferentAccess.new( dockerfiles: ::Gitlab::Template::DockerfileTemplate, gitignores: ::Gitlab::Template::GitignoreTemplate, gitlab_ci_ymls: ::Gitlab::Template::GitlabCiYmlTemplate - }.freeze + ).freeze class << self - def build(type, params = {}) - if type == :licenses - LicenseTemplateFinder.new(params) # rubocop: disable CodeReuse/Finder + def build(type, project, params = {}) + if type.to_s == 'licenses' + LicenseTemplateFinder.new(project, params) # rubocop: disable CodeReuse/Finder else - new(type, params) + new(type, project, params) end end end - attr_reader :type, :params + attr_reader :type, :project, :params attr_reader :vendored_templates private :vendored_templates - def initialize(type, params = {}) + def initialize(type, project, params = {}) @type = type + @project = project @params = params @vendored_templates = VENDORED_TEMPLATES.fetch(type) diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 9cbd5b5f785..883e5ddff57 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -159,10 +159,6 @@ module BlobHelper end end - def licenses_for_select - @licenses_for_select ||= template_dropdown_names(TemplateFinder.build(:licenses).execute) - end - def ref_project @ref_project ||= @target_project || @project end @@ -173,29 +169,34 @@ module BlobHelper categories.each_with_object({}) do |category, hash| hash[category] = grouped[category].map do |item| - { name: item.name, id: item.id } + { name: item.name, id: item.key } end end end private :template_dropdown_names - def gitignore_names - @gitignore_names ||= template_dropdown_names(TemplateFinder.build(:gitignores).execute) + def licenses_for_select(project = @project) + @licenses_for_select ||= template_dropdown_names(TemplateFinder.build(:licenses, project).execute) + end + + def gitignore_names(project = @project) + @gitignore_names ||= template_dropdown_names(TemplateFinder.build(:gitignores, project).execute) end - def gitlab_ci_ymls - @gitlab_ci_ymls ||= template_dropdown_names(TemplateFinder.build(:gitlab_ci_ymls).execute) + def gitlab_ci_ymls(project = @project) + @gitlab_ci_ymls ||= template_dropdown_names(TemplateFinder.build(:gitlab_ci_ymls, project).execute) end - def dockerfile_names - @dockerfile_names ||= template_dropdown_names(TemplateFinder.build(:dockerfiles).execute) + def dockerfile_names(project = @project) + @dockerfile_names ||= template_dropdown_names(TemplateFinder.build(:dockerfiles, project).execute) end - def blob_editor_paths + def blob_editor_paths(project = @project) { 'relative-url-root' => Rails.application.config.relative_url_root, 'assets-prefix' => Gitlab::Application.config.assets.prefix, - 'blob-language' => @blob && @blob.language.try(:ace_mode) + 'blob-language' => @blob && @blob.language.try(:ace_mode), + 'project-id' => project.id } end diff --git a/app/models/license_template.rb b/app/models/license_template.rb index 693a6a89fd2..73e403f98b4 100644 --- a/app/models/license_template.rb +++ b/app/models/license_template.rb @@ -12,12 +12,10 @@ class LicenseTemplate (fullname|name\sof\s(author|copyright\sowner)) [\>\}\]]}xi.freeze - attr_reader :id, :name, :category, :nickname, :url, :meta + attr_reader :key, :name, :category, :nickname, :url, :meta - alias_method :key, :id - - def initialize(id:, name:, category:, content:, nickname: nil, url: nil, meta: {}) - @id = id + def initialize(key:, name:, category:, content:, nickname: nil, url: nil, meta: {}) + @key = key @name = name @category = category @content = content diff --git a/changelogs/unreleased/5987-project-templates-api.yml b/changelogs/unreleased/5987-project-templates-api.yml new file mode 100644 index 00000000000..a627ba9f0de --- /dev/null +++ b/changelogs/unreleased/5987-project-templates-api.yml @@ -0,0 +1,5 @@ +--- +title: Allow file templates to be requested at the project level +merge_request: 7776 +author: +type: added diff --git a/doc/api/README.md b/doc/api/README.md index a3589377e9d..a351db99bbd 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -20,10 +20,11 @@ following locations: - [Custom Attributes](custom_attributes.md) - [Deployments](deployments.md) - [Deploy Keys](deploy_keys.md) +- [Dockerfile templates](templates/dockerfiles.md) - [Environments](environments.md) - [Events](events.md) - [Feature flags](features.md) -- [Gitignores templates](templates/gitignores.md) +- [Gitignore templates](templates/gitignores.md) - [GitLab CI Config templates](templates/gitlab_ci_ymls.md) - [Groups](groups.md) - [Group Access Requests](access_requests.md) @@ -55,6 +56,7 @@ following locations: - [Project import/export](project_import_export.md) - [Project Members](members.md) - [Project Snippets](project_snippets.md) +- [Project Templates](project_templates.md) - [Protected Branches](protected_branches.md) - [Protected Tags](protected_tags.md) - [Repositories](repositories.md) diff --git a/doc/api/project_templates.md b/doc/api/project_templates.md new file mode 100644 index 00000000000..ebdfa975849 --- /dev/null +++ b/doc/api/project_templates.md @@ -0,0 +1,135 @@ +# Project templates API + +This API is a project-specific implementation of these endpoints: + +- [Dockerfile templates](templates/dockerfiles.md) +- [Gitignore templates](templates/gitignores.md) +- [GitLab CI Config templates](templates/gitlab_ci_ymls.md) +- [Open source license templates](templates/licenses.md) + +It deprecates those endpoints, which will be removed for API version 5. + +Project-specific templates will be added to this API in time. This includes, but +is not limited to: + +- [Issue and Merge Request templates](../user/project/description_templates.html) +- [Group level file templates](https://gitlab.com/gitlab-org/gitlab-ee/issues/5987) **(Premium)** + +## Get all templates of a particular type + +``` +GET /projects/:id/templates/:type +``` + +| Attribute | Type | Required | Description | +| ---------- | ------ | -------- | ----------- | +| `id ` | integer / string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | +| `type` | string | yes| The type `(dockerfiles|gitignores|gitlab_ci_ymls|licenses)` of the template | + +Example response (licenses): + +```json +[ + { + "key": "epl-1.0", + "name": "Eclipse Public License 1.0" + }, + { + "key": "lgpl-3.0", + "name": "GNU Lesser General Public License v3.0" + }, + { + "key": "unlicense", + "name": "The Unlicense" + }, + { + "key": "agpl-3.0", + "name": "GNU Affero General Public License v3.0" + }, + { + "key": "gpl-3.0", + "name": "GNU General Public License v3.0" + }, + { + "key": "bsd-3-clause", + "name": "BSD 3-clause \"New\" or \"Revised\" License" + }, + { + "key": "lgpl-2.1", + "name": "GNU Lesser General Public License v2.1" + }, + { + "key": "mit", + "name": "MIT License" + }, + { + "key": "apache-2.0", + "name": "Apache License 2.0" + }, + { + "key": "bsd-2-clause", + "name": "BSD 2-clause \"Simplified\" License" + }, + { + "key": "mpl-2.0", + "name": "Mozilla Public License 2.0" + }, + { + "key": "gpl-2.0", + "name": "GNU General Public License v2.0" + } +] +``` + +## Get one template of a particular type + +``` +GET /projects/:id/templates/:type/:key +``` + +| Attribute | Type | Required | Description | +| ---------- | ------ | -------- | ----------- | +| `id ` | integer / string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | +| `type` | string | yes| The type `(dockerfiles|gitignores|gitlab_ci_ymls|licenses)` of the template | +| `key` | string | yes | The key of the template, as obtained from the collection endpoint | +| `project` | string | no | The project name to use when expanding placeholders in the template. Only affects licenses | +| `fullname` | string | no | The full name of the copyright holder to use when expanding placeholders in the template. Only affects licenses | + +Example response (Dockerfile): + + +```json +{ + "name": "Binary", + "content": "# This file is a template, and might need editing before it works on your project.\n# This Dockerfile installs a compiled binary into a bare system.\n# You must either commit your compiled binary into source control (not recommended)\n# or build the binary first as part of a CI/CD pipeline.\n\nFROM buildpack-deps:jessie\n\nWORKDIR /usr/local/bin\n\n# Change `app` to whatever your binary is called\nAdd app .\nCMD [\"./app\"]\n" +} + +``` + +Example response (license): + +```json +{ + "key": "mit", + "name": "MIT License", + "nickname": null, + "popular": true, + "html_url": "http://choosealicense.com/licenses/mit/", + "source_url": "https://opensource.org/licenses/MIT", + "description": "A short and simple permissive license with conditions only requiring preservation of copyright and license notices. Licensed works, modifications, and larger works may be distributed under different terms and without source code.", + "conditions": [ + "include-copyright" + ], + "permissions": [ + "commercial-use", + "modifications", + "distribution", + "private-use" + ], + "limitations": [ + "liability", + "warranty" + ], + "content": "MIT License\n\nCopyright (c) 2018 [fullname]\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" +} +``` diff --git a/doc/api/templates/dockerfiles.md b/doc/api/templates/dockerfiles.md new file mode 100644 index 00000000000..a08b8d33693 --- /dev/null +++ b/doc/api/templates/dockerfiles.md @@ -0,0 +1,113 @@ +# Dockerfiles API + +## List Dockerfile templates + +Get all Dockerfile templates. + +``` +GET /templates/dockerfiles +``` + +```bash +curl https://gitlab.example.com/api/v4/templates/dockerfiles +``` + +Example response: + +```json +[ + { + "key": "Binary", + "name": "Binary" + }, + { + "key": "Binary-alpine", + "name": "Binary-alpine" + }, + { + "key": "Binary-scratch", + "name": "Binary-scratch" + }, + { + "key": "Golang", + "name": "Golang" + }, + { + "key": "Golang-alpine", + "name": "Golang-alpine" + }, + { + "key": "Golang-scratch", + "name": "Golang-scratch" + }, + { + "key": "HTTPd", + "name": "HTTPd" + }, + { + "key": "Node", + "name": "Node" + }, + { + "key": "Node-alpine", + "name": "Node-alpine" + }, + { + "key": "OpenJDK", + "name": "OpenJDK" + }, + { + "key": "OpenJDK-alpine", + "name": "OpenJDK-alpine" + }, + { + "key": "PHP", + "name": "PHP" + }, + { + "key": "Python", + "name": "Python" + }, + { + "key": "Python-alpine", + "name": "Python-alpine" + }, + { + "key": "Python2", + "name": "Python2" + }, + { + "key": "Ruby", + "name": "Ruby" + }, + { + "key": "Ruby-alpine", + "name": "Ruby-alpine" + } +] +``` + +## Single Dockerfile template + +Get a single Dockerfile template. + +``` +GET /templates/dockerfiles/:key +``` + +| Attribute | Type | Required | Description | +| ---------- | ------ | -------- | ----------- | +| `key` | string | yes | The key of the Dockerfile template | + +```bash +curl https://gitlab.example.com/api/v4/templates/dockerfiles/Binary +``` + +Example response: + +```json +{ + "name": "Binary", + "content": "# This file is a template, and might need editing before it works on your project.\n# This Dockerfile installs a compiled binary into a bare system.\n# You must either commit your compiled binary into source control (not recommended)\n# or build the binary first as part of a CI/CD pipeline.\n\nFROM buildpack-deps:jessie\n\nWORKDIR /usr/local/bin\n\n# Change `app` to whatever your binary is called\nAdd app .\nCMD [\"./app\"]\n" +} +``` diff --git a/doc/api/templates/gitignores.md b/doc/api/templates/gitignores.md index d3f5c88ca90..3804855129c 100644 --- a/doc/api/templates/gitignores.md +++ b/doc/api/templates/gitignores.md @@ -17,538 +17,84 @@ Example response: ```json [ { - "name": "AppEngine" - }, - { - "name": "Laravel" - }, - { - "name": "Elisp" - }, - { - "name": "SketchUp" + "key": "Actionscript", + "name": "Actionscript" }, { + "key": "Ada", "name": "Ada" }, { - "name": "Ruby" - }, - { - "name": "Kohana" - }, - { - "name": "Nanoc" - }, - { - "name": "Erlang" - }, - { - "name": "OCaml" - }, - { - "name": "Lithium" - }, - { - "name": "Fortran" - }, - { - "name": "Scala" - }, - { - "name": "Node" - }, - { - "name": "Fancy" - }, - { - "name": "Perl" - }, - { - "name": "Zephir" - }, - { - "name": "WordPress" - }, - { - "name": "Symfony" - }, - { - "name": "FuelPHP" - }, - { - "name": "DM" - }, - { - "name": "Sdcc" - }, - { - "name": "Rust" - }, - { - "name": "C" - }, - { - "name": "Umbraco" - }, - { - "name": "Actionscript" + "key": "Agda", + "name": "Agda" }, { + "key": "Android", "name": "Android" }, { - "name": "Grails" - }, - { - "name": "Composer" - }, - { - "name": "ExpressionEngine" - }, - { - "name": "Gcov" - }, - { - "name": "Qt" + "key": "AppEngine", + "name": "AppEngine" }, { - "name": "Phalcon" + "key": "AppceleratorTitanium", + "name": "AppceleratorTitanium" }, { + "key": "ArchLinuxPackages", "name": "ArchLinuxPackages" }, { - "name": "TeX" - }, - { - "name": "SCons" - }, - { - "name": "Lilypond" - }, - { - "name": "CommonLisp" - }, - { - "name": "Rails" - }, - { - "name": "Mercury" - }, - { - "name": "Magento" - }, - { - "name": "ChefCookbook" - }, - { - "name": "GitBook" - }, - { - "name": "C++" - }, - { - "name": "Eagle" - }, - { - "name": "Go" - }, - { - "name": "OpenCart" - }, - { - "name": "Scheme" - }, - { - "name": "Typo3" - }, - { - "name": "SeamGen" - }, - { - "name": "Swift" - }, - { - "name": "Elm" - }, - { - "name": "Unity" - }, - { - "name": "Agda" - }, - { - "name": "CUDA" - }, - { - "name": "VVVV" - }, - { - "name": "Finale" - }, - { - "name": "LemonStand" - }, - { - "name": "Textpattern" - }, - { - "name": "Julia" - }, - { - "name": "Packer" - }, - { - "name": "Scrivener" - }, - { - "name": "Dart" - }, - { - "name": "Plone" - }, - { - "name": "Jekyll" - }, - { - "name": "Xojo" - }, - { - "name": "LabVIEW" - }, - { + "key": "Autotools", "name": "Autotools" }, { - "name": "KiCad" - }, - { - "name": "Prestashop" - }, - { - "name": "ROS" - }, - { - "name": "Smalltalk" - }, - { - "name": "GWT" - }, - { - "name": "OracleForms" - }, - { - "name": "SugarCRM" - }, - { - "name": "Nim" - }, - { - "name": "SymphonyCMS" + "key": "C", + "name": "C" }, { - "name": "Maven" + "key": "C++", + "name": "C++" }, { + "key": "CFWheels", "name": "CFWheels" }, { - "name": "Python" - }, - { - "name": "ZendFramework" - }, - { - "name": "CakePHP" - }, - { - "name": "Concrete5" - }, - { - "name": "PlayFramework" - }, - { - "name": "Terraform" - }, - { - "name": "Elixir" - }, - { + "key": "CMake", "name": "CMake" }, { - "name": "Joomla" - }, - { - "name": "Coq" - }, - { - "name": "Delphi" - }, - { - "name": "Haskell" - }, - { - "name": "Yii" - }, - { - "name": "Java" - }, - { - "name": "UnrealEngine" - }, - { - "name": "AppceleratorTitanium" - }, - { - "name": "CraftCMS" - }, - { - "name": "ForceDotCom" - }, - { - "name": "ExtJs" - }, - { - "name": "MetaProgrammingSystem" - }, - { - "name": "D" - }, - { - "name": "Objective-C" - }, - { - "name": "RhodesRhomobile" - }, - { - "name": "R" - }, - { - "name": "EPiServer" - }, - { - "name": "Yeoman" - }, - { - "name": "VisualStudio" - }, - { - "name": "Processing" - }, - { - "name": "Leiningen" - }, - { - "name": "Stella" - }, - { - "name": "Opa" - }, - { - "name": "Drupal" - }, - { - "name": "TurboGears2" - }, - { - "name": "Idris" - }, - { - "name": "Jboss" - }, - { - "name": "CodeIgniter" - }, - { - "name": "Qooxdoo" - }, - { - "name": "Waf" + "key": "CUDA", + "name": "CUDA" }, { - "name": "Sass" + "key": "CakePHP", + "name": "CakePHP" }, { - "name": "Lua" + "key": "ChefCookbook", + "name": "ChefCookbook" }, { + "key": "Clojure", "name": "Clojure" }, { - "name": "IGORPro" - }, - { - "name": "Gradle" - }, - { - "name": "Archives" - }, - { - "name": "SynopsysVCS" - }, - { - "name": "Ninja" - }, - { - "name": "Tags" - }, - { - "name": "OSX" - }, - { - "name": "Dreamweaver" - }, - { - "name": "CodeKit" - }, - { - "name": "NotepadPP" - }, - { - "name": "VisualStudioCode" - }, - { - "name": "Mercurial" - }, - { - "name": "BricxCC" - }, - { - "name": "DartEditor" - }, - { - "name": "Eclipse" - }, - { - "name": "Cloud9" - }, - { - "name": "TortoiseGit" - }, - { - "name": "NetBeans" - }, - { - "name": "GPG" - }, - { - "name": "Espresso" - }, - { - "name": "Redcar" - }, - { - "name": "Xcode" - }, - { - "name": "Matlab" - }, - { - "name": "LyX" - }, - { - "name": "SlickEdit" - }, - { - "name": "Dropbox" - }, - { - "name": "CVS" - }, - { - "name": "Calabash" - }, - { - "name": "JDeveloper" - }, - { - "name": "Vagrant" - }, - { - "name": "IPythonNotebook" - }, - { - "name": "TextMate" - }, - { - "name": "Ensime" - }, - { - "name": "WebMethods" - }, - { - "name": "VirtualEnv" - }, - { - "name": "Emacs" - }, - { - "name": "Momentics" - }, - { - "name": "JetBrains" - }, - { - "name": "SublimeText" - }, - { - "name": "Kate" - }, - { - "name": "ModelSim" - }, - { - "name": "Redis" - }, - { - "name": "KDevelop4" - }, - { - "name": "Bazaar" - }, - { - "name": "Linux" - }, - { - "name": "Windows" - }, - { - "name": "XilinxISE" - }, - { - "name": "Lazarus" - }, - { - "name": "EiffelStudio" - }, - { - "name": "Anjuta" - }, - { - "name": "Vim" - }, - { - "name": "Otto" - }, - { - "name": "MicrosoftOffice" - }, - { - "name": "LibreOffice" - }, - { - "name": "SBT" + "key": "CodeIgniter", + "name": "CodeIgniter" }, { - "name": "MonoDevelop" + "key": "CommonLisp", + "name": "CommonLisp" }, { - "name": "SVN" + "key": "Composer", + "name": "Composer" }, { - "name": "FlexBuilder" + "key": "Concrete5", + "name": "Concrete5" } ] ``` diff --git a/doc/api/templates/gitlab_ci_ymls.md b/doc/api/templates/gitlab_ci_ymls.md index bdb128fc336..cecfc8cd9b9 100644 --- a/doc/api/templates/gitlab_ci_ymls.md +++ b/doc/api/templates/gitlab_ci_ymls.md @@ -17,79 +17,84 @@ Example response: ```json [ { - "name": "C++" - }, - { - "name": "Docker" - }, - { - "name": "Elixir" - }, - { - "name": "LaTeX" - }, - { - "name": "Grails" - }, - { - "name": "Rust" + "key": "Android", + "name": "Android" }, { - "name": "Nodejs" + "key": "Auto-DevOps", + "name": "Auto-DevOps" }, { - "name": "Ruby" + "key": "Bash", + "name": "Bash" }, { - "name": "Scala" + "key": "C++", + "name": "C++" }, { - "name": "Maven" + "key": "Chef", + "name": "Chef" }, { - "name": "Harp" + "key": "Clojure", + "name": "Clojure" }, { - "name": "Pelican" + "key": "Crystal", + "name": "Crystal" }, { - "name": "Hyde" + "key": "Django", + "name": "Django" }, { - "name": "Nanoc" + "key": "Docker", + "name": "Docker" }, { - "name": "Octopress" + "key": "Elixir", + "name": "Elixir" }, { - "name": "JBake" + "key": "Go", + "name": "Go" }, { - "name": "HTML" + "key": "Gradle", + "name": "Gradle" }, { - "name": "Hugo" + "key": "Grails", + "name": "Grails" }, { - "name": "Metalsmith" + "key": "Julia", + "name": "Julia" }, { - "name": "Hexo" + "key": "LaTeX", + "name": "LaTeX" }, { - "name": "Lektor" + "key": "Laravel", + "name": "Laravel" }, { - "name": "Doxygen" + "key": "Maven", + "name": "Maven" }, { - "name": "Brunch" + "key": "Mono", + "name": "Mono" }, { - "name": "Jekyll" + "key": "Nodejs", + "name": "Nodejs" }, { - "name": "Middleman" + "key": "OpenShift", + "name": "OpenShift" } ] ``` diff --git a/lib/api/api.rb b/lib/api/api.rb index 06c8b48b8cc..c49c52213bf 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -132,6 +132,7 @@ module API mount ::API::Projects mount ::API::ProjectSnapshots mount ::API::ProjectSnippets + mount ::API::ProjectTemplates mount ::API::ProtectedBranches mount ::API::ProtectedTags mount ::API::Repositories diff --git a/lib/api/entities.rb b/lib/api/entities.rb index a78a93cbfd9..120545792f2 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -1221,6 +1221,7 @@ module API end class TemplatesList < Grape::Entity + expose :key expose :name end diff --git a/lib/api/project_templates.rb b/lib/api/project_templates.rb new file mode 100644 index 00000000000..d05ddad7466 --- /dev/null +++ b/lib/api/project_templates.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module API + class ProjectTemplates < Grape::API + include PaginationParams + + TEMPLATE_TYPES = %w[dockerfiles gitignores gitlab_ci_ymls licenses].freeze + + before { authenticate_non_get! } + + params do + requires :id, type: String, desc: 'The ID of a project' + requires :type, type: String, values: TEMPLATE_TYPES, desc: 'The type (dockerfiles|gitignores|gitlab_ci_ymls|licenses) of the template' + end + resource :projects do + desc 'Get a list of templates available to this project' do + detail 'This endpoint was introduced in GitLab 11.4' + end + params do + use :pagination + end + get ':id/templates/:type' do + templates = TemplateFinder + .build(params[:type], user_project) + .execute + + present paginate(::Kaminari.paginate_array(templates)), with: Entities::TemplatesList + end + + desc 'Download a template available to this project' do + detail 'This endpoint was introduced in GitLab 11.4' + end + params do + requires :name, type: String, desc: 'The name of the template' + + optional :project, type: String, desc: 'The project name to use when expanding placeholders in the template. Only affects licenses' + optional :fullname, type: String, desc: 'The full name of the copyright holder to use when expanding placeholders in the template. Only affects licenses' + end + get ':id/templates/:type/:name', requirements: { name: /[\w\.-]+/ } do + template = TemplateFinder + .build(params[:type], user_project, name: params[:name]) + .execute + + not_found!('Template') unless template.present? + + template.resolve!( + project_name: params[:project].presence, + fullname: params[:fullname].presence || current_user&.name + ) + + if template.is_a?(::LicenseTemplate) + present template, with: Entities::License + else + present template, with: Entities::Template + end + end + end + end +end diff --git a/lib/api/templates.rb b/lib/api/templates.rb index 8ff3b2ac33c..8dab19d50c2 100644 --- a/lib/api/templates.rb +++ b/lib/api/templates.rb @@ -35,7 +35,7 @@ module API popular = declared(params)[:popular] popular = to_boolean(popular) if popular.present? - templates = TemplateFinder.build(:licenses, popular: popular).execute + templates = TemplateFinder.build(:licenses, nil, popular: popular).execute present paginate(::Kaminari.paginate_array(templates)), with: ::API::Entities::License end @@ -48,8 +48,7 @@ module API requires :name, type: String, desc: 'The name of the template' end get "templates/licenses/:name", requirements: { name: /[\w\.-]+/ } do - templates = TemplateFinder.build(:licenses).execute - template = templates.find { |template| template.key == params[:name] } + template = TemplateFinder.build(:licenses, nil, name: params[:name]).execute not_found!('License') unless template.present? @@ -72,7 +71,7 @@ module API use :pagination end get "templates/#{template_type}" do - templates = ::Kaminari.paginate_array(TemplateFinder.new(template_type).execute) + templates = ::Kaminari.paginate_array(TemplateFinder.build(template_type, nil).execute) present paginate(templates), with: Entities::TemplatesList end @@ -84,7 +83,7 @@ module API requires :name, type: String, desc: 'The name of the template' end get "templates/#{template_type}/:name" do - finder = TemplateFinder.build(template_type, name: declared(params)[:name]) + finder = TemplateFinder.build(template_type, nil, name: declared(params)[:name]) new_template = finder.execute render_response(template_type, new_template) diff --git a/lib/gitlab/template/base_template.rb b/lib/gitlab/template/base_template.rb index 3770f3f250b..4456217017f 100644 --- a/lib/gitlab/template/base_template.rb +++ b/lib/gitlab/template/base_template.rb @@ -12,14 +12,21 @@ module Gitlab def name File.basename(@path, self.class.extension) end - alias_method :id, :name + alias_method :key, :name def content @finder.read(@path) end + # Present for compatibility with license templates, which can replace text + # like `[fullname]` with a user-specified string. This is a no-op for + # other templates + def resolve!(_placeholders = {}) + self + end + def to_json - { name: name, content: content } + { key: key, name: name, content: content } end def <=>(other) diff --git a/spec/finders/license_template_finder_spec.rb b/spec/finders/license_template_finder_spec.rb index a97903103c9..f6f40bf33cc 100644 --- a/spec/finders/license_template_finder_spec.rb +++ b/spec/finders/license_template_finder_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe LicenseTemplateFinder do describe '#execute' do - subject(:result) { described_class.new(params).execute } + subject(:result) { described_class.new(nil, params).execute } let(:categories) { categorised_licenses.keys } let(:categorised_licenses) { result.group_by(&:category) } @@ -31,7 +31,7 @@ describe LicenseTemplateFinder do it 'returns all licenses known by the Licensee gem' do from_licensee = Licensee::License.all.map { |l| l.key } - expect(result.map(&:id)).to match_array(from_licensee) + expect(result.map(&:key)).to match_array(from_licensee) end it 'correctly copies all attributes' do diff --git a/spec/finders/template_finder_spec.rb b/spec/finders/template_finder_spec.rb index 1d399e8194f..114af9461e0 100644 --- a/spec/finders/template_finder_spec.rb +++ b/spec/finders/template_finder_spec.rb @@ -4,6 +4,8 @@ describe TemplateFinder do using RSpec::Parameterized::TableSyntax describe '#build' do + let(:project) { build_stubbed(:project) } + where(:type, :expected_class) do :dockerfiles | described_class :gitignores | described_class @@ -12,9 +14,10 @@ describe TemplateFinder do end with_them do - subject { described_class.build(type) } + subject(:finder) { described_class.build(type, project) } it { is_expected.to be_a(expected_class) } + it { expect(finder.project).to eq(project) } end end @@ -27,19 +30,19 @@ describe TemplateFinder do with_them do it 'returns all vendored templates when no name is specified' do - result = described_class.new(type).execute + result = described_class.new(type, nil).execute expect(result).to include(have_attributes(name: vendored_name)) end it 'returns only the specified vendored template when a name is specified' do - result = described_class.new(type, name: vendored_name).execute + result = described_class.new(type, nil, name: vendored_name).execute expect(result).to have_attributes(name: vendored_name) end it 'returns nil when an unknown name is specified' do - result = described_class.new(type, name: 'unknown').execute + result = described_class.new(type, nil, name: 'unknown').execute expect(result).to be_nil end diff --git a/spec/fixtures/api/schemas/public_api/v4/license.json b/spec/fixtures/api/schemas/public_api/v4/license.json new file mode 100644 index 00000000000..38c8c3e9192 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/license.json @@ -0,0 +1,30 @@ +{ + "type": "object", + "required": [ + "key", + "name", + "nickname", + "popular", + "html_url", + "source_url", + "description", + "conditions", + "permissions", + "limitations", + "content" + ], + "properties": { + "key": { "type": "string" }, + "name": { "type": "string" }, + "nickname": { "type": ["null", "string"] }, + "popular": { "type": "boolean" }, + "html_url": { "type": ["null", "string"] }, + "source_url": { "type": ["null", "string"] }, + "description": { "type": ["null", "string"] }, + "conditions": { "type": "array", "items": { "type": "string" } }, + "permissions": { "type": "array", "items": { "type": "string" } }, + "limitations": { "type": "array", "items": { "type": "string" } }, + "content": { "type": "string" } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/public_api/v4/template.json b/spec/fixtures/api/schemas/public_api/v4/template.json new file mode 100644 index 00000000000..38601aa6b45 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/template.json @@ -0,0 +1,12 @@ +{ + "type": "object", + "required": [ + "name", + "content" + ], + "properties": { + "name": { "type": "string" }, + "content": { "type": "string" } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/public_api/v4/template_list.json b/spec/fixtures/api/schemas/public_api/v4/template_list.json new file mode 100644 index 00000000000..2336dafb17b --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/template_list.json @@ -0,0 +1,15 @@ +{ + "type": "array", + "items": { + "type": "object", + "required": [ + "key", + "name" + ], + "properties": { + "key": { "type": "string" }, + "name": { "type": "string" } + }, + "additionalProperties": false + } +} diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js index 54cb6d84109..091edf13cfe 100644 --- a/spec/javascripts/api_spec.js +++ b/spec/javascripts/api_spec.js @@ -250,71 +250,45 @@ describe('Api', () => { }); }); - describe('licenseText', () => { - it('fetches a license text', done => { - const licenseKey = "driver's license"; - const data = { unused: 'option' }; - const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/licenses/${licenseKey}`; + describe('issueTemplate', () => { + it('fetches an issue template', done => { + const namespace = 'some namespace'; + const project = 'some project'; + const templateKey = ' template #%?.key '; + const templateType = 'template type'; + const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/templates/${templateType}/${encodeURIComponent( + templateKey, + )}`; mock.onGet(expectedUrl).reply(200, 'test'); - Api.licenseText(licenseKey, data, response => { + Api.issueTemplate(namespace, project, templateKey, templateType, (error, response) => { expect(response).toBe('test'); done(); }); }); }); - describe('gitignoreText', () => { - it('fetches a gitignore text', done => { - const gitignoreKey = 'ignore git'; - const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/gitignores/${gitignoreKey}`; - mock.onGet(expectedUrl).reply(200, 'test'); - - Api.gitignoreText(gitignoreKey, response => { - expect(response).toBe('test'); - done(); - }); - }); - }); + describe('projectTemplates', () => { + it('fetches a list of templates', done => { + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/gitlab-org%2Fgitlab-ce/templates/licenses`; - describe('gitlabCiYml', () => { - it('fetches a .gitlab-ci.yml', done => { - const gitlabCiYmlKey = 'Y CI ML'; - const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/gitlab_ci_ymls/${gitlabCiYmlKey}`; mock.onGet(expectedUrl).reply(200, 'test'); - Api.gitlabCiYml(gitlabCiYmlKey, response => { + Api.projectTemplates('gitlab-org/gitlab-ce', 'licenses', {}, response => { expect(response).toBe('test'); done(); }); }); }); - describe('dockerfileYml', () => { - it('fetches a Dockerfile', done => { - const dockerfileYmlKey = 'a giant whale'; - const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/dockerfiles/${dockerfileYmlKey}`; - mock.onGet(expectedUrl).reply(200, 'test'); - - Api.dockerfileYml(dockerfileYmlKey, response => { - expect(response).toBe('test'); - done(); - }); - }); - }); + describe('projectTemplate', () => { + it('fetches a single template', done => { + const data = { unused: 'option' }; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/gitlab-org%2Fgitlab-ce/templates/licenses/test%20license`; - describe('issueTemplate', () => { - it('fetches an issue template', done => { - const namespace = 'some namespace'; - const project = 'some project'; - const templateKey = ' template #%?.key '; - const templateType = 'template type'; - const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/templates/${templateType}/${encodeURIComponent( - templateKey, - )}`; mock.onGet(expectedUrl).reply(200, 'test'); - Api.issueTemplate(namespace, project, templateKey, templateType, (error, response) => { + Api.projectTemplate('gitlab-org/gitlab-ce', 'licenses', 'test license', data, response => { expect(response).toBe('test'); done(); }); diff --git a/spec/models/license_template_spec.rb b/spec/models/license_template_spec.rb index c633e1908d4..dd912eefac1 100644 --- a/spec/models/license_template_spec.rb +++ b/spec/models/license_template_spec.rb @@ -54,6 +54,6 @@ describe LicenseTemplate do end def build_template(content) - described_class.new(id: 'foo', name: 'foo', category: :Other, content: content) + described_class.new(key: 'foo', name: 'foo', category: :Other, content: content) end end diff --git a/spec/requests/api/project_templates_spec.rb b/spec/requests/api/project_templates_spec.rb new file mode 100644 index 00000000000..86e33f23951 --- /dev/null +++ b/spec/requests/api/project_templates_spec.rb @@ -0,0 +1,145 @@ +require 'spec_helper' + +describe API::ProjectTemplates do + let(:public_project) { create(:project, :public) } + let(:private_project) { create(:project, :private) } + let(:developer) { create(:user) } + + before do + private_project.add_developer(developer) + end + + describe 'GET /projects/:id/templates/:type' do + it 'returns dockerfiles' do + get api("/projects/#{public_project.id}/templates/dockerfiles") + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(response).to match_response_schema('public_api/v4/template_list') + expect(json_response).to satisfy_one { |template| template['key'] == 'Binary' } + end + + it 'returns gitignores' do + get api("/projects/#{public_project.id}/templates/gitignores") + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(response).to match_response_schema('public_api/v4/template_list') + expect(json_response).to satisfy_one { |template| template['key'] == 'Actionscript' } + end + + it 'returns gitlab_ci_ymls' do + get api("/projects/#{public_project.id}/templates/gitlab_ci_ymls") + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(response).to match_response_schema('public_api/v4/template_list') + expect(json_response).to satisfy_one { |template| template['key'] == 'Android' } + end + + it 'returns licenses' do + get api("/projects/#{public_project.id}/templates/licenses") + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(response).to match_response_schema('public_api/v4/template_list') + expect(json_response).to satisfy_one { |template| template['key'] == 'mit' } + end + + it 'returns 400 for an unknown template type' do + get api("/projects/#{public_project.id}/templates/unknown") + + expect(response).to have_gitlab_http_status(400) + end + + it 'denies access to an anonymous user on a private project' do + get api("/projects/#{private_project.id}/templates/licenses") + + expect(response).to have_gitlab_http_status(404) + end + + it 'permits access to a developer on a private project' do + get api("/projects/#{private_project.id}/templates/licenses", developer) + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/template_list') + end + end + + describe 'GET /projects/:id/templates/licenses' do + it 'returns key and name for the listed licenses' do + get api("/projects/#{public_project.id}/templates/licenses") + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/template_list') + end + end + + describe 'GET /projects/:id/templates/:type/:key' do + it 'returns a specific dockerfile' do + get api("/projects/#{public_project.id}/templates/dockerfiles/Binary") + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/template') + expect(json_response['name']).to eq('Binary') + end + + it 'returns a specific gitignore' do + get api("/projects/#{public_project.id}/templates/gitignores/Actionscript") + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/template') + expect(json_response['name']).to eq('Actionscript') + end + + it 'returns a specific gitlab_ci_yml' do + get api("/projects/#{public_project.id}/templates/gitlab_ci_ymls/Android") + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/template') + expect(json_response['name']).to eq('Android') + end + + it 'returns a specific license' do + get api("/projects/#{public_project.id}/templates/licenses/mit") + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/license') + end + + it 'returns 404 for an unknown specific template' do + get api("/projects/#{public_project.id}/templates/licenses/unknown") + + expect(response).to have_gitlab_http_status(404) + end + + it 'denies access to an anonymous user on a private project' do + get api("/projects/#{private_project.id}/templates/licenses/mit") + + expect(response).to have_gitlab_http_status(404) + end + + it 'permits access to a developer on a private project' do + get api("/projects/#{private_project.id}/templates/licenses/mit", developer) + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/license') + end + end + + describe 'GET /projects/:id/templates/licenses/:key' do + it 'fills placeholders in the license' do + get api("/projects/#{public_project.id}/templates/licenses/agpl-3.0"), + project: 'Project Placeholder', + fullname: 'Fullname Placeholder' + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/license') + + content = json_response['content'] + + expect(content).to include('Project Placeholder') + expect(content).to include("Copyright (C) #{Time.now.year} Fullname Placeholder") + end + end +end |