diff options
author | Shinya Maeda <shinya@gitlab.com> | 2018-06-01 14:32:40 +0900 |
---|---|---|
committer | Shinya Maeda <shinya@gitlab.com> | 2018-06-01 14:32:40 +0900 |
commit | 272b8dca80eddfcf40933ad5295e9d11f8b1fa93 (patch) | |
tree | 9cafcf1ce1643fbfa1d3237e1af3a569f48d8459 | |
parent | 0e22b50df8b269ccae32ab68b9ba26e7eea861b0 (diff) | |
parent | 50fda506e3bc93af47c8ebcbfc7d38c7ae55467d (diff) | |
download | gitlab-ce-272b8dca80eddfcf40933ad5295e9d11f8b1fa93.tar.gz |
Merge branch 'master' into per-project-pipeline-iid
422 files changed, 4478 insertions, 18273 deletions
diff --git a/.codeclimate.yml b/.codeclimate.yml index 8699a903f2a..9998ddba643 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -8,8 +8,6 @@ engines: languages: - ruby - javascript - exclude_paths: - - "lib/api/v3/*" ratings: paths: - Gemfile.lock diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 3f187db0c07..00000000000 --- a/.eslintrc +++ /dev/null @@ -1,56 +0,0 @@ -{ - "env": { - "browser": true, - "es6": true - }, - "extends": [ - "airbnb-base", - "plugin:vue/recommended" - ], - "globals": { - "__webpack_public_path__": true, - "gl": false, - "gon": false, - "localStorage": false - }, - "parserOptions": { - "parser": "babel-eslint" - }, - "plugins": [ - "filenames", - "import", - "html", - "promise" - ], - "settings": { - "html/html-extensions": [".html", ".html.raw"], - "import/resolver": { - "webpack": { - "config": "./config/webpack.config.js" - } - } - }, - "rules": { - "filenames/match-regex": [2, "^[a-z0-9_]+$"], - "import/no-commonjs": "error", - "no-multiple-empty-lines": ["error", { "max": 1 }], - "promise/catch-or-return": "error", - "no-underscore-dangle": ["error", { "allow": ["__", "_links"] }], - "no-mixed-operators": 0, - "space-before-function-paren": 0, - "curly": 0, - "arrow-parens": 0, - "vue/html-self-closing": [ - "error", - { - "html": { - "void": "always", - "normal": "never", - "component": "always" - }, - "svg": "always", - "math": "always" - } - ] - } -} diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 00000000000..f851e3b67e6 --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,77 @@ +--- +env: + browser: true + es6: true +extends: + - airbnb-base + - plugin:vue/recommended +globals: + __webpack_public_path__: true + gl: false + gon: false + localStorage: false +parserOptions: + parser: babel-eslint +plugins: + - filenames + - import + - html + - promise +settings: + html/html-extensions: + - ".html" + - ".html.raw" + import/resolver: + webpack: + config: "./config/webpack.config.js" +rules: + filenames/match-regex: + - error + - "^[a-z0-9_]+$" + import/no-commonjs: error + no-multiple-empty-lines: + - error + - max: 1 + promise/catch-or-return: error + no-underscore-dangle: + - error + - allow: + - __ + - _links + no-mixed-operators: off + vue/html-self-closing: + - error + - html: + void: always + normal: never + component: always + svg: always + math: always + ## Conflicting rules with prettier: + space-before-function-paren: off + curly: off + arrow-parens: off + function-paren-newline: off + object-curly-newline: off + padded-blocks: off + # Disabled for now, to make the eslint 3 -> eslint 4 update smoother + ## Indent rule. We are using the old for now: https://eslint.org/docs/user-guide/migrating-to-4.0.0#indent-rewrite + indent: off + indent-legacy: + - error + - 2 + - SwitchCase: 1 + VariableDeclarator: 1 + outerIIFEBody: 1 + FunctionDeclaration: + parameters: 1 + body: 1 + FunctionExpression: + parameters: 1 + body: 1 + ## Destructuring: https://eslint.org/docs/rules/prefer-destructuring + prefer-destructuring: off + ## no-restricted-globals: https://eslint.org/docs/rules/no-restricted-globals + no-restricted-globals: off + ## no-multi-assign: https://eslint.org/docs/rules/no-multi-assign + no-multi-assign: off diff --git a/.gitignore b/.gitignore index c7d1648615d..51b77d5ac9e 100644 --- a/.gitignore +++ b/.gitignore @@ -64,6 +64,7 @@ eslint-report.html /tags /tmp/* /vendor/bundle/* +/vendor/gitaly-ruby /builds* /shared/* /.gitlab_workhorse_secret diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0b2ee4b1cd8..d3daab78940 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -591,7 +591,7 @@ ee_compat_check: except: - master - tags - - /^[\d-]+-stable(-ee)?/ + - /[\d-]+-stable(-ee)?/ - /^security-/ - branches@gitlab-org/gitlab-ee - branches@gitlab/gitlab-ee diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 16b0b5c95e2..1fb352306d7 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -173,7 +173,6 @@ Lint/UriEscapeUnescape: - 'spec/requests/api/files_spec.rb' - 'spec/requests/api/internal_spec.rb' - 'spec/requests/api/issues_spec.rb' - - 'spec/requests/api/v3/issues_spec.rb' # Offense count: 1 # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. @@ -333,8 +332,6 @@ RSpec/ScatteredSetup: - 'spec/lib/gitlab/bitbucket_import/importer_spec.rb' - 'spec/lib/gitlab/git/env_spec.rb' - 'spec/requests/api/jobs_spec.rb' - - 'spec/requests/api/v3/builds_spec.rb' - - 'spec/requests/api/v3/projects_spec.rb' - 'spec/services/projects/create_service_spec.rb' # Offense count: 1 @@ -618,7 +615,6 @@ Style/OrAssignment: Exclude: - 'app/models/concerns/token_authenticatable.rb' - 'lib/api/commit_statuses.rb' - - 'lib/api/v3/members.rb' - 'lib/gitlab/project_transfer.rb' # Offense count: 50 @@ -781,7 +777,6 @@ Style/TernaryParentheses: - 'app/finders/projects_finder.rb' - 'app/helpers/namespaces_helper.rb' - 'features/support/capybara.rb' - - 'lib/api/v3/projects.rb' - 'lib/gitlab/ci/build/artifacts/metadata/entry.rb' - 'spec/requests/api/pipeline_schedules_spec.rb' - 'spec/support/capybara.rb' diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bc941cafaa..ec92829f7d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 10.8.3 (2018-05-30) + +### Fixed (4 changes) + +- Replace Gitlab::REVISION with Gitlab.revision and handle installations without a .git directory. !19125 +- Fix encoding of branch names on compare and new merge request page. !19143 +- Fix remote mirror database inconsistencies when upgrading from EE to CE. !19196 +- Fix local storage not being cleared after creating a new issue. + +### Performance (1 change) + +- Memoize Gitlab::Database.version. + + ## 10.8.2 (2018-05-28) ### Security (3 changes) diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 89eba2c5b85..e49057b3302 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.103.0 +0.104.0 @@ -28,7 +28,7 @@ gem 'mysql2', '~> 0.4.10', group: :mysql gem 'pg', '~> 0.18.2', group: :postgres gem 'rugged', '~> 0.27' -gem 'grape-route-helpers', '~> 2.1.0' +gem 'grape-path-helpers', '~> 1.0' gem 'faraday', '~> 0.12' @@ -144,6 +144,9 @@ gem 'truncato', '~> 0.7.9' gem 'bootstrap_form', '~> 2.7.0' gem 'nokogiri', '~> 1.8.2' +# Calendar rendering +gem 'icalendar' + # Diffs gem 'diffy', '~> 3.1.0' @@ -219,7 +222,7 @@ gem 'asana', '~> 0.6.0' gem 'ruby-fogbugz', '~> 0.2.1' # Kubernetes integration -gem 'kubeclient', '~> 3.0' +gem 'kubeclient', '~> 3.1.0' # Sanitize user input gem 'sanitize', '~> 2.0' @@ -320,7 +323,7 @@ group :development, :test do gem 'pry-byebug', '~> 3.4.1', platform: :mri gem 'pry-rails', '~> 0.3.4' - gem 'awesome_print', '~> 1.2.0', require: false + gem 'awesome_print', require: false gem 'fuubar', '~> 2.2.0' gem 'database_cleaner', '~> 1.5.0' diff --git a/Gemfile.lock b/Gemfile.lock index 915e1c530fb..4d2bd62bec0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -69,7 +69,7 @@ GEM attr_encrypted (3.1.0) encryptor (~> 3.0.0) attr_required (1.0.0) - awesome_print (1.2.0) + awesome_print (1.8.0) axiom-types (0.1.1) descendants_tracker (~> 0.0.4) ice_nine (~> 0.11.0) @@ -168,7 +168,7 @@ GEM diff-lcs (1.3) diffy (3.1.0) docile (1.1.5) - domain_name (0.5.20170404) + domain_name (0.5.20180417) unf (>= 0.0.5, < 1.0.0) doorkeeper (4.3.2) railties (>= 4.2) @@ -348,7 +348,7 @@ GEM signet (~> 0.7) gpgme (2.0.13) mini_portile2 (~> 2.1) - grape (1.0.2) + grape (1.0.3) activesupport builder mustermann-grape (~> 1.0.0) @@ -358,10 +358,10 @@ GEM grape-entity (0.7.1) activesupport (>= 4.0) multi_json (>= 1.3.2) - grape-route-helpers (2.1.0) - activesupport - grape (>= 0.16.0) - rake + grape-path-helpers (1.0.1) + activesupport (~> 4) + grape (~> 1.0) + rake (~> 12) grape_logging (1.7.0) grape grpc (1.11.0) @@ -410,6 +410,7 @@ GEM httpclient (2.8.3) i18n (0.9.5) concurrent-ruby (~> 1.0) + icalendar (2.4.1) ice_nine (0.11.2) influxdb (0.2.3) cause @@ -446,9 +447,9 @@ GEM knapsack (1.16.0) rake timecop (>= 0.1.0) - kubeclient (3.0.0) + kubeclient (3.1.0) http (~> 2.2.2) - recursive-open-struct (~> 1.0.4) + recursive-open-struct (~> 1.0, >= 1.0.4) rest-client (~> 2.0) launchy (2.4.3) addressable (~> 2.3) @@ -698,7 +699,7 @@ GEM re2 (1.1.1) recaptcha (3.0.0) json - recursive-open-struct (1.0.5) + recursive-open-struct (1.1.0) redcarpet (3.4.0) redis (3.3.5) redis-actionpack (5.0.2) @@ -977,7 +978,7 @@ DEPENDENCIES asciidoctor-plantuml (= 0.0.8) asset_sync (~> 2.4) attr_encrypted (~> 3.1.0) - awesome_print (~> 1.2.0) + awesome_print babosa (~> 1.0.2) base32 (~> 0.3.0) batch-loader (~> 1.2.1) @@ -1049,7 +1050,7 @@ DEPENDENCIES gpgme grape (~> 1.0) grape-entity (~> 0.7.1) - grape-route-helpers (~> 2.1.0) + grape-path-helpers (~> 1.0) grape_logging (~> 1.7) grpc (~> 1.11.0) haml_lint (~> 0.26.0) @@ -1060,6 +1061,7 @@ DEPENDENCIES html-pipeline (~> 2.7.1) html2text httparty (~> 0.13.3) + icalendar influxdb (~> 0.2) jira-ruby (~> 1.4) jquery-atwho-rails (~> 1.3.2) @@ -1067,7 +1069,7 @@ DEPENDENCIES jwt (~> 1.5.6) kaminari (~> 1.0) knapsack (~> 1.16) - kubeclient (~> 3.0) + kubeclient (~> 3.1.0) letter_opener_web (~> 1.3.0) license_finder (~> 3.1) licensee (~> 8.9) diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js b/app/assets/javascripts/boards/components/modal/empty_state.js index eb8a66975ee..1e5f2383223 100644 --- a/app/assets/javascripts/boards/components/modal/empty_state.js +++ b/app/assets/javascripts/boards/components/modal/empty_state.js @@ -41,10 +41,10 @@ gl.issueBoards.ModalEmptyState = Vue.extend({ template: ` <section class="empty-state"> <div class="row"> - <div class="col-xs-12 col-sm-6 order-sm-last"> + <div class="col-12 col-md-6 order-md-last"> <aside class="svg-content"><img :src="emptyStateSvg"/></aside> </div> - <div class="col-xs-12 col-sm-6 order-sm-first"> + <div class="col-12 col-md-6 order-md-first"> <div class="text-content"> <h4>{{ contents.title }}</h4> <p v-html="contents.content"></p> diff --git a/app/assets/javascripts/boards/components/modal/list.js b/app/assets/javascripts/boards/components/modal/list.js index 6c662432037..f86896d2178 100644 --- a/app/assets/javascripts/boards/components/modal/list.js +++ b/app/assets/javascripts/boards/components/modal/list.js @@ -1,5 +1,3 @@ -/* global ListIssue */ - import Vue from 'vue'; import bp from '../../../breakpoints'; import ModalStore from '../../stores/modal_store'; @@ -56,8 +54,11 @@ gl.issueBoards.ModalList = Vue.extend({ scrollHandler() { const currentPage = Math.floor(this.issues.length / this.perPage); - if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage - && currentPage === this.page) { + if ( + this.scrollTop() > this.scrollHeight() - 100 && + !this.loadingNewPage && + currentPage === this.page + ) { this.loadingNewPage = true; this.page += 1; } diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue index 371774098b9..eb335f352d3 100644 --- a/app/assets/javascripts/boards/components/project_select.vue +++ b/app/assets/javascripts/boards/components/project_select.vue @@ -1,71 +1,69 @@ <script> - /* global ListIssue */ +import $ from 'jquery'; +import _ from 'underscore'; +import eventHub from '../eventhub'; +import loadingIcon from '../../vue_shared/components/loading_icon.vue'; +import Api from '../../api'; - import $ from 'jquery'; - import _ from 'underscore'; - import eventHub from '../eventhub'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; - import Api from '../../api'; - - export default { - name: 'BoardProjectSelect', - components: { - loadingIcon, - }, - props: { - groupId: { - type: Number, - required: true, - default: 0, - }, +export default { + name: 'BoardProjectSelect', + components: { + loadingIcon, + }, + props: { + groupId: { + type: Number, + required: true, + default: 0, }, - data() { - return { - loading: true, - selectedProject: {}, - }; + }, + data() { + return { + loading: true, + selectedProject: {}, + }; + }, + computed: { + selectedProjectName() { + return this.selectedProject.name || 'Select a project'; }, - computed: { - selectedProjectName() { - return this.selectedProject.name || 'Select a project'; + }, + mounted() { + $(this.$refs.projectsDropdown).glDropdown({ + filterable: true, + filterRemote: true, + search: { + fields: ['name_with_namespace'], }, - }, - mounted() { - $(this.$refs.projectsDropdown).glDropdown({ - filterable: true, - filterRemote: true, - search: { - fields: ['name_with_namespace'], - }, - clicked: ({ $el, e }) => { - e.preventDefault(); - this.selectedProject = { - id: $el.data('project-id'), - name: $el.data('project-name'), - }; - eventHub.$emit('setSelectedProject', this.selectedProject); - }, - selectable: true, - data: (term, callback) => { - this.loading = true; - return Api.groupProjects(this.groupId, term, (projects) => { - this.loading = false; - callback(projects); - }); - }, - renderRow(project) { - return ` + clicked: ({ $el, e }) => { + e.preventDefault(); + this.selectedProject = { + id: $el.data('project-id'), + name: $el.data('project-name'), + }; + eventHub.$emit('setSelectedProject', this.selectedProject); + }, + selectable: true, + data: (term, callback) => { + this.loading = true; + return Api.groupProjects(this.groupId, term, projects => { + this.loading = false; + callback(projects); + }); + }, + renderRow(project) { + return ` <li> <a href='#' class='dropdown-menu-link' data-project-id="${project.id}" data-project-name="${project.name}"> ${_.escape(project.name)} </a> </li> `; - }, - text: project => project.name, - }); - }, - }; + }, + text: project => project.name, + }); + }, +}; </script> <template> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue index 705953c86e3..e2b42ab2642 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue @@ -126,7 +126,6 @@ export default { </div> <form v-if="!isCompact" - class="form-horizontal" @submit.prevent.stop="commitChanges" ref="formEl" > diff --git a/app/assets/javascripts/ide/components/external_link.vue b/app/assets/javascripts/ide/components/external_link.vue new file mode 100644 index 00000000000..cf3316a8179 --- /dev/null +++ b/app/assets/javascripts/ide/components/external_link.vue @@ -0,0 +1,41 @@ +<script> +import Icon from '~/vue_shared/components/icon.vue'; + +export default { + components: { + Icon, + }, + props: { + file: { + type: Object, + required: true, + }, + }, + computed: { + showButtons() { + return this.file.permalink; + }, + }, +}; +</script> + +<template> + <div + v-if="showButtons" + class="pull-right ide-btn-group" + > + <a + :href="file.permalink" + target="_blank" + :title="s__('IDE|Open in file view')" + rel="noopener noreferrer" + > + <span class="vertical-align-middle">Open in file view</span> + <icon + name="external-link" + css-classes="vertical-align-middle space-right" + :size="16" + /> + </a> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/ide_file_buttons.vue b/app/assets/javascripts/ide/components/ide_file_buttons.vue deleted file mode 100644 index 30b00abf6ed..00000000000 --- a/app/assets/javascripts/ide/components/ide_file_buttons.vue +++ /dev/null @@ -1,84 +0,0 @@ -<script> -import { __ } from '~/locale'; -import tooltip from '~/vue_shared/directives/tooltip'; -import Icon from '~/vue_shared/components/icon.vue'; - -export default { - components: { - Icon, - }, - directives: { - tooltip, - }, - props: { - file: { - type: Object, - required: true, - }, - }, - computed: { - showButtons() { - return ( - this.file.rawPath || this.file.blamePath || this.file.commitsPath || this.file.permalink - ); - }, - rawDownloadButtonLabel() { - return this.file.binary ? __('Download') : __('Raw'); - }, - }, -}; -</script> - -<template> - <div - v-if="showButtons" - class="float-right ide-btn-group" - > - <a - v-tooltip - v-if="!file.binary" - :href="file.blamePath" - :title="__('Blame')" - class="btn btn-sm btn-transparent blame" - > - <icon - name="blame" - :size="16" - /> - </a> - <a - v-tooltip - :href="file.commitsPath" - :title="__('History')" - class="btn btn-sm btn-transparent history" - > - <icon - name="history" - :size="16" - /> - </a> - <a - v-tooltip - :href="file.permalink" - :title="__('Permalink')" - class="btn btn-sm btn-transparent permalink" - > - <icon - name="link" - :size="16" - /> - </a> - <a - v-tooltip - :href="file.rawPath" - target="_blank" - class="btn btn-sm btn-transparent prepend-left-10 raw" - rel="noopener noreferrer" - :title="rawDownloadButtonLabel"> - <icon - name="download" - :size="16" - /> - </a> - </div> -</template> diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index a281ecb95c8..93453989c08 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -6,12 +6,12 @@ import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer import { activityBarViews, viewerTypes } from '../constants'; import monacoLoader from '../monaco_loader'; import Editor from '../lib/editor'; -import IdeFileButtons from './ide_file_buttons.vue'; +import ExternalLink from './external_link.vue'; export default { components: { ContentViewer, - IdeFileButtons, + ExternalLink, }, props: { file: { @@ -224,7 +224,7 @@ export default { </a> </li> </ul> - <ide-file-buttons + <external-link :file="file" /> </div> diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js index b1e43a1e38c..e5149b1f3ad 100644 --- a/app/assets/javascripts/ide/lib/common/model.js +++ b/app/assets/javascripts/ide/lib/common/model.js @@ -1,4 +1,3 @@ -/* global monaco */ import Disposable from './disposable'; import eventHub from '../../eventhub'; diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index 13aea91d8ba..74f9c112f5a 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -84,11 +84,11 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive }); }; -export const setFileMrChange = ({ state, commit }, { file, mrChange }) => { +export const setFileMrChange = ({ commit }, { file, mrChange }) => { commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange }); }; -export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) => { +export const getRawFileData = ({ state, commit }, { path, baseSha }) => { const file = state.entries[path]; return new Promise((resolve, reject) => { service @@ -156,7 +156,7 @@ export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn } }; -export const setFileViewMode = ({ state, commit }, { file, viewMode }) => { +export const setFileViewMode = ({ commit }, { file, viewMode }) => { commit(types.SET_FILE_VIEWMODE, { file, viewMode }); }; diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js index da73034fd7d..5ec9bd661bb 100644 --- a/app/assets/javascripts/ide/stores/actions/merge_request.js +++ b/app/assets/javascripts/ide/stores/actions/merge_request.js @@ -3,7 +3,7 @@ import service from '../../services'; import * as types from '../mutation_types'; export const getMergeRequestData = ( - { commit, state, dispatch }, + { commit, state }, { projectId, mergeRequestId, force = false } = {}, ) => new Promise((resolve, reject) => { @@ -32,7 +32,7 @@ export const getMergeRequestData = ( }); export const getMergeRequestChanges = ( - { commit, state, dispatch }, + { commit, state }, { projectId, mergeRequestId, force = false } = {}, ) => new Promise((resolve, reject) => { @@ -58,7 +58,7 @@ export const getMergeRequestChanges = ( }); export const getMergeRequestVersions = ( - { commit, state, dispatch }, + { commit, state }, { projectId, mergeRequestId, force = false } = {}, ) => new Promise((resolve, reject) => { diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index cece9154c82..75cfd9946d7 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -7,10 +7,7 @@ import Poll from '../../../lib/utils/poll'; let eTagPoll; -export const getProjectData = ( - { commit, state, dispatch }, - { namespace, projectId, force = false } = {}, -) => +export const getProjectData = ({ commit, state }, { namespace, projectId, force = false } = {}) => new Promise((resolve, reject) => { if (!state.projects[`${namespace}/${projectId}`] || force) { commit(types.TOGGLE_LOADING, { entry: state }); @@ -40,10 +37,7 @@ export const getProjectData = ( } }); -export const getBranchData = ( - { commit, state, dispatch }, - { projectId, branchId, force = false } = {}, -) => +export const getBranchData = ({ commit, state }, { projectId, branchId, force = false } = {}) => new Promise((resolve, reject) => { if ( typeof state.projects[`${projectId}`] === 'undefined' || @@ -78,7 +72,7 @@ export const getBranchData = ( } }); -export const refreshLastCommitData = ({ commit, state, dispatch }, { projectId, branchId } = {}) => +export const refreshLastCommitData = ({ commit }, { projectId, branchId } = {}) => service .getBranchData(projectId, branchId) .then(({ data }) => { @@ -92,7 +86,7 @@ export const refreshLastCommitData = ({ commit, state, dispatch }, { projectId, flash(__('Error loading last commit.'), 'alert', document, null, false, true); }); -export const pollSuccessCallBack = ({ commit, state, dispatch }, { data }) => { +export const pollSuccessCallBack = ({ commit, state }, { data }) => { if (data.pipelines && data.pipelines.length) { const lastCommitHash = state.projects[state.currentProjectId].branches[state.currentBranchId].commit.id; diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js index 6536be04f0a..cc5116413f7 100644 --- a/app/assets/javascripts/ide/stores/actions/tree.js +++ b/app/assets/javascripts/ide/stores/actions/tree.js @@ -5,7 +5,7 @@ import * as types from '../mutation_types'; import { findEntry } from '../utils'; import FilesDecoratorWorker from '../workers/files_decorator_worker'; -export const toggleTreeOpen = ({ commit, dispatch }, path) => { +export const toggleTreeOpen = ({ commit }, path) => { commit(types.TOGGLE_TREE_OPEN, path); }; @@ -23,7 +23,7 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => { } }; -export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => { +export const getLastCommitData = ({ state, commit, dispatch }, tree = state) => { if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return; service @@ -49,7 +49,7 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s .catch(() => flash('Error fetching log data.', 'alert', document, null, false, true)); }; -export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = {}) => +export const getFiles = ({ state, commit }, { projectId, branchId } = {}) => new Promise((resolve, reject) => { if (!state.trees[`${projectId}/${branchId}`]) { const selectedProject = state.projects[projectId]; diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index cd25c3060f2..0a0db4033c8 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -31,9 +31,9 @@ export const setLastCommitMessage = ({ rootState, commit }, data) => { const currentProject = rootState.projects[rootState.currentProjectId]; const commitStats = data.stats ? sprintf(__('with %{additions} additions, %{deletions} deletions.'), { - additions: data.stats.additions, // eslint-disable-line indent - deletions: data.stats.deletions, // eslint-disable-line indent - }) // eslint-disable-line indent + additions: data.stats.additions, // eslint-disable-line indent-legacy + deletions: data.stats.deletions, // eslint-disable-line indent-legacy + }) // eslint-disable-line indent-legacy : ''; const commitMsg = sprintf( __('Your changes have been committed. Commit %{commitId} %{commitStats}'), @@ -74,10 +74,7 @@ export const checkCommitStatus = ({ rootState }) => ), ); -export const updateFilesAfterCommit = ( - { commit, dispatch, state, rootState, rootGetters }, - { data }, -) => { +export const updateFilesAfterCommit = ({ commit, dispatch, rootState }, { data }) => { const selectedProject = rootState.projects[rootState.currentProjectId]; const lastCommit = { commit_path: `${selectedProject.web_url}/commit/${data.id}`, diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 90d4e19e90b..bb8b3d91e40 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -30,7 +30,7 @@ export default class IssuableForm { } this.initAutosave(); - this.form.on('submit:success', this.handleSubmit); + this.form.on('submit', this.handleSubmit); this.form.on('click', '.btn-cancel', this.resetAutosave); this.initWip(); diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js index c67bd7fb0c6..611e8200b4d 100644 --- a/app/assets/javascripts/job.js +++ b/app/assets/javascripts/job.js @@ -84,7 +84,7 @@ export default class Job { If the browser does not support position sticky, it returns the position as static. If the browser does support sticky, then we allow the browser to handle it, if not then we use a polyfill - **/ + */ if (this.$topBar.css('position') !== 'static') return; StickyFill.add(this.$topBar); diff --git a/app/assets/javascripts/jobs/job_details_mediator.js b/app/assets/javascripts/jobs/job_details_mediator.js index 5a216f8fae2..89019da9d1e 100644 --- a/app/assets/javascripts/jobs/job_details_mediator.js +++ b/app/assets/javascripts/jobs/job_details_mediator.js @@ -1,5 +1,3 @@ -/* global Build */ - import Visibility from 'visibilityjs'; import Flash from '../flash'; import Poll from '../lib/utils/poll'; @@ -50,7 +48,8 @@ export default class JobMediator { } getJob() { - return this.service.getJob() + return this.service + .getJob() .then(response => this.successCallback(response)) .catch(() => this.errorCallback()); } diff --git a/app/assets/javascripts/locale/index.js b/app/assets/javascripts/locale/index.js index 2f4328b56e1..2cc5fb10027 100644 --- a/app/assets/javascripts/locale/index.js +++ b/app/assets/javascripts/locale/index.js @@ -9,7 +9,7 @@ delete window.translations; Translates `text` @param text The text to be translated @returns {String} The translated text -**/ +*/ const gettext = locale.gettext.bind(locale); /** @@ -21,7 +21,7 @@ const gettext = locale.gettext.bind(locale); @param pluralText Plural text to translate (eg. '%d days') @param count Number to decide which translation to use (eg. 2) @returns {String} Translated text with the number replaced (eg. '2 days') -**/ +*/ const ngettext = (text, pluralText, count) => { const translated = locale.ngettext(text, pluralText, count).replace(/%d/g, count).split('|'); @@ -38,7 +38,7 @@ const ngettext = (text, pluralText, count) => { (eg. 'Context') @param key Is the dynamic variable you want to be translated @returns {String} Translated context based text -**/ +*/ const pgettext = (keyOrContext, key) => { const normalizedKey = key ? `${keyOrContext}|${key}` : keyOrContext; const translated = gettext(normalizedKey).split('|'); diff --git a/app/assets/javascripts/locale/sprintf.js b/app/assets/javascripts/locale/sprintf.js index 5f4a053f98e..599104dcfa0 100644 --- a/app/assets/javascripts/locale/sprintf.js +++ b/app/assets/javascripts/locale/sprintf.js @@ -10,7 +10,7 @@ import _ from 'underscore'; @see https://ruby-doc.org/core-2.3.3/Kernel.html#method-i-sprintf @see https://gitlab.com/gitlab-org/gitlab-ce/issues/37992 -**/ +*/ export default (input, parameters, escapeParameters = true) => { let output = input; diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index bac7d966ecc..493c119dc6f 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -427,7 +427,7 @@ export default class MergeRequestTabs { If the browser does not support position sticky, it returns the position as static. If the browser does support sticky, then we allow the browser to handle it, if not then we default back to Bootstraps affix - **/ + */ if ($tabs.css('position') !== 'static') return; const $diffTabs = $('#diff-notes-app'); diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 98ce070288e..b2222476924 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -12,20 +12,13 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils'; let eTagPoll; -export const setNotesData = ({ commit }, data) => - commit(types.SET_NOTES_DATA, data); -export const setNoteableData = ({ commit }, data) => - commit(types.SET_NOTEABLE_DATA, data); -export const setUserData = ({ commit }, data) => - commit(types.SET_USER_DATA, data); -export const setLastFetchedAt = ({ commit }, data) => - commit(types.SET_LAST_FETCHED_AT, data); -export const setInitialNotes = ({ commit }, data) => - commit(types.SET_INITIAL_NOTES, data); -export const setTargetNoteHash = ({ commit }, data) => - commit(types.SET_TARGET_NOTE_HASH, data); -export const toggleDiscussion = ({ commit }, data) => - commit(types.TOGGLE_DISCUSSION, data); +export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data); +export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_DATA, data); +export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data); +export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data); +export const setInitialNotes = ({ commit }, data) => commit(types.SET_INITIAL_NOTES, data); +export const setTargetNoteHash = ({ commit }, data) => commit(types.SET_TARGET_NOTE_HASH, data); +export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data); export const fetchNotes = ({ commit }, path) => service @@ -69,20 +62,14 @@ export const createNewNote = ({ commit }, { endpoint, data }) => return res; }); -export const removePlaceholderNotes = ({ commit }) => - commit(types.REMOVE_PLACEHOLDER_NOTES); +export const removePlaceholderNotes = ({ commit }) => commit(types.REMOVE_PLACEHOLDER_NOTES); -export const toggleResolveNote = ( - { commit }, - { endpoint, isResolved, discussion }, -) => +export const toggleResolveNote = ({ commit }, { endpoint, isResolved, discussion }) => service .toggleResolveNote(endpoint, isResolved) .then(res => res.json()) .then(res => { - const mutationType = discussion - ? types.UPDATE_DISCUSSION - : types.UPDATE_NOTE; + const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE; commit(mutationType, res); }); @@ -114,7 +101,7 @@ export const reopenIssue = ({ commit, dispatch, state }) => { export const toggleStateButtonLoading = ({ commit }, value) => commit(types.TOGGLE_STATE_BUTTON_LOADING, value); -export const emitStateChangedEvent = ({ commit, getters }, data) => { +export const emitStateChangedEvent = ({ getters }, data) => { const event = new CustomEvent('issuable_vue_app:change', { detail: { data, @@ -179,10 +166,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { loadAwardsHandler() .then(awardsHandler => { - awardsHandler.addAwardToEmojiBar( - votesBlock, - commandsChanges.emoji_award, - ); + awardsHandler.addAwardToEmojiBar(votesBlock, commandsChanges.emoji_award); awardsHandler.scrollToAwards(); }) .catch(() => { @@ -194,10 +178,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { }); } - if ( - commandsChanges.spend_time != null || - commandsChanges.time_estimate != null - ) { + if (commandsChanges.spend_time != null || commandsChanges.time_estimate != null) { sidebarTimeTrackingEventHub.$emit('timeTrackingUpdated', res); } } @@ -218,14 +199,8 @@ const pollSuccessCallBack = (resp, commit, state, getters) => { resp.notes.forEach(note => { if (notesById[note.id]) { commit(types.UPDATE_NOTE, note); - } else if ( - note.type === constants.DISCUSSION_NOTE || - note.type === constants.DIFF_NOTE - ) { - const discussion = utils.findNoteObjectById( - state.notes, - note.discussion_id, - ); + } else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) { + const discussion = utils.findNoteObjectById(state.notes, note.discussion_id); if (discussion) { commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note); @@ -249,11 +224,8 @@ export const poll = ({ commit, state, getters }) => { method: 'poll', data: state, successCallback: resp => - resp - .json() - .then(data => pollSuccessCallBack(data, commit, state, getters)), - errorCallback: () => - Flash('Something went wrong while fetching latest comments.'), + resp.json().then(data => pollSuccessCallBack(data, commit, state, getters)), + errorCallback: () => Flash('Something went wrong while fetching latest comments.'), }); if (!Visibility.hidden()) { @@ -292,14 +264,11 @@ export const fetchData = ({ commit, state, getters }) => { .catch(() => Flash('Something went wrong while fetching latest comments.')); }; -export const toggleAward = ( - { commit, state, getters, dispatch }, - { awardName, noteId }, -) => { +export const toggleAward = ({ commit, getters }, { awardName, noteId }) => { commit(types.TOGGLE_AWARD, { awardName, note: getters.notesById[noteId] }); }; -export const toggleAwardRequest = ({ commit, getters, dispatch }, data) => { +export const toggleAwardRequest = ({ dispatch }, data) => { const { endpoint, awardName } = data; return service diff --git a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue index df21e2f8771..5765eed4d45 100644 --- a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue +++ b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue @@ -59,7 +59,7 @@ export default { ref="form" :action="deleteWikiUrl" method="post" - class="form-horizontal js-requires-input" + class="js-requires-input" > <input ref="method" diff --git a/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js index 53030045292..18c7b21cf8c 100644 --- a/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js +++ b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js @@ -5,7 +5,7 @@ import $ from 'jquery'; * * Toggling this checkbox adds/removes a `remember_me` parameter to the * login buttons' href, which is passed on to the omniauth callback. - **/ + */ export default class OAuthRememberMe { constructor(opts = {}) { diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue index 7bfe11ab8cd..e64afc94ef9 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue @@ -79,12 +79,13 @@ export default { }; </script> <template> - <div class="ci-job-dropdown-container dropdown"> + <div class="ci-job-dropdown-container dropdown dropright"> <button v-tooltip type="button" data-toggle="dropdown" data-container="body" + data-boundary="viewport" class="dropdown-menu-toggle build-content" :title="tooltipText" > diff --git a/app/assets/javascripts/raven/raven_config.js b/app/assets/javascripts/raven/raven_config.js index ae54fa5f1a9..658caeecde1 100644 --- a/app/assets/javascripts/raven/raven_config.js +++ b/app/assets/javascripts/raven/raven_config.js @@ -37,7 +37,7 @@ const IGNORE_URLS = [ /extensions\//i, /^chrome:\/\//i, // Other plugins - /127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb + /127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb /webappstoolbarba\.texthelp\.com\//i, /metrics\.itunes\.apple\.com\.edgesuite\.net\//i, ]; diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js index 593a43c7cc1..c0de03373d8 100644 --- a/app/assets/javascripts/registry/stores/actions.js +++ b/app/assets/javascripts/registry/stores/actions.js @@ -7,9 +7,10 @@ Vue.use(VueResource); export const fetchRepos = ({ commit, state }) => { commit(types.TOGGLE_MAIN_LOADING); - return Vue.http.get(state.endpoint) + return Vue.http + .get(state.endpoint) .then(res => res.json()) - .then((response) => { + .then(response => { commit(types.TOGGLE_MAIN_LOADING); commit(types.SET_REPOS_LIST, response); }); @@ -18,19 +19,20 @@ export const fetchRepos = ({ commit, state }) => { export const fetchList = ({ commit }, { repo, page }) => { commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo); - return Vue.http.get(repo.tagsPath, { params: { page } }) - .then((response) => { - const headers = response.headers; + return Vue.http.get(repo.tagsPath, { params: { page } }).then(response => { + const headers = response.headers; - return response.json().then((resp) => { - commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo); - commit(types.SET_REGISTRY_LIST, { repo, resp, headers }); - }); + return response.json().then(resp => { + commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo); + commit(types.SET_REGISTRY_LIST, { repo, resp, headers }); }); + }); }; +// eslint-disable-next-line no-unused-vars export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.destroyPath); +// eslint-disable-next-line no-unused-vars export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath); export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data); diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue index 8c7e0664559..eb581b807a2 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue @@ -168,8 +168,8 @@ <a :href="mr.mergeCommitPath" class="commit-sha js-mr-merged-commit-sha" + v-text="mr.shortMergeCommitSha" > - {{ mr.shortMergeCommitSha }} </a> <clipboard-button :title="__('Copy commit SHA to clipboard')" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue index 926a3172412..875c3323dbb 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue @@ -1,15 +1,63 @@ -/* -The squash-before-merge button is EE only, but it's located right in the middle -of the readyToMerge state component template. - -If we didn't declare this component in CE, we'd need to maintain a separate copy -of the readyToMergeState template in EE, which is pretty big and likely to change. - -Instead, in CE, we declare the component, but it's hidden and is configured to do nothing. -In EE, the configuration extends this object to add a functioning squash-before-merge -button. -*/ - <script> - export default {}; +import Icon from '~/vue_shared/components/icon.vue'; +import eventHub from '~/vue_merge_request_widget/event_hub'; +import tooltip from '~/vue_shared/directives/tooltip'; + +export default { + components: { + Icon, + }, + directives: { + tooltip, + }, + props: { + mr: { + type: Object, + required: true, + }, + isMergeButtonDisabled: { + type: Boolean, + required: true, + }, + }, + data() { + return { + squashBeforeMerge: this.mr.squash, + }; + }, + methods: { + updateSquashModel() { + eventHub.$emit('MRWidgetUpdateSquash', this.squashBeforeMerge); + }, + }, +}; </script> + +<template> + <div class="accept-control inline"> + <label class="merge-param-checkbox"> + <input + type="checkbox" + name="squash" + class="qa-squash-checkbox" + :disabled="isMergeButtonDisabled" + v-model="squashBeforeMerge" + @change="updateSquashModel" + /> + {{ __('Squash commits') }} + </label> + <a + :href="mr.squashBeforeMergeHelpPath" + data-title="About this feature" + data-placement="bottom" + target="_blank" + rel="noopener noreferrer nofollow" + data-container="body" + v-tooltip + > + <icon + name="question-o" + /> + </a> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index 1d1c8ebc179..3a194320bd8 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -6,11 +6,13 @@ import MergeRequest from '../../../merge_request'; import Flash from '../../../flash'; import statusIcon from '../mr_widget_status_icon.vue'; import eventHub from '../../event_hub'; +import SquashBeforeMerge from './mr_widget_squash_before_merge.vue'; export default { name: 'ReadyToMerge', components: { statusIcon, + 'squash-before-merge': SquashBeforeMerge, }, props: { mr: { type: Object, required: true }, @@ -101,6 +103,12 @@ export default { return enableSquashBeforeMerge && commitsCount > 1; }, }, + created() { + eventHub.$on('MRWidgetUpdateSquash', this.handleUpdateSquash); + }, + beforeDestroy() { + eventHub.$off('MRWidgetUpdateSquash', this.handleUpdateSquash); + }, methods: { shouldShowMergeControls() { return this.mr.isMergeAllowed || this.shouldShowMergeWhenPipelineSucceedsText; @@ -128,13 +136,9 @@ export default { commit_message: this.commitMessage, merge_when_pipeline_succeeds: this.setToMergeWhenPipelineSucceeds, should_remove_source_branch: this.removeSourceBranch === true, + squash: this.mr.squash, }; - // Only truthy in EE extension of this component - if (this.setAdditionalParams) { - this.setAdditionalParams(options); - } - this.isMakingRequest = true; this.service.merge(options) .then(res => res.data) @@ -154,6 +158,9 @@ export default { new Flash('Something went wrong. Please try again.'); // eslint-disable-line }); }, + handleUpdateSquash(val) { + this.mr.squash = val; + }, initiateMergePolling() { simplePoll((continuePolling, stopPolling) => { this.handleMergePolling(continuePolling, stopPolling); diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index 83b7b054e6f..e5b7e1f1c68 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -15,6 +15,11 @@ export default class MergeRequestStore { const currentUser = data.current_user; const pipelineStatus = data.pipeline ? data.pipeline.details.status : null; + this.squash = data.squash; + this.squashBeforeMergeHelpPath = this.squashBeforeMergeHelpPath || + data.squash_before_merge_help_path; + this.enableSquashBeforeMerge = this.enableSquashBeforeMerge || true; + this.title = data.title; this.targetBranch = data.target_branch; this.sourceBranch = data.source_branch; diff --git a/app/assets/javascripts/vue_shared/directives/tooltip.js b/app/assets/javascripts/vue_shared/directives/tooltip.js index 155e6b6698a..4f2412ce520 100644 --- a/app/assets/javascripts/vue_shared/directives/tooltip.js +++ b/app/assets/javascripts/vue_shared/directives/tooltip.js @@ -2,7 +2,9 @@ import $ from 'jquery'; export default { bind(el) { - $(el).tooltip(); + $(el).tooltip({ + trigger: 'hover', + }); }, componentUpdated(el) { diff --git a/app/assets/javascripts/vue_shared/translate.js b/app/assets/javascripts/vue_shared/translate.js index 2c7886ec308..48c63373b77 100644 --- a/app/assets/javascripts/vue_shared/translate.js +++ b/app/assets/javascripts/vue_shared/translate.js @@ -13,7 +13,7 @@ export default (Vue) => { @param text The text to be translated @returns {String} The translated text - **/ + */ __, /** Translate the text with a number @@ -24,7 +24,7 @@ export default (Vue) => { @param pluralText Plural text to translate (eg. '%d days') @param count Number to decide which translation to use (eg. 2) @returns {String} Translated text with the number replaced (eg. '2 days') - **/ + */ n__, /** Translate context based text @@ -36,7 +36,7 @@ export default (Vue) => { (eg. 'Context') @param key Is the dynamic variable you want to be translated @returns {String} Translated context based text - **/ + */ s__, sprintf, }, diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss index e1a47f3d686..4a4da027c92 100644 --- a/app/assets/stylesheets/bootstrap_migration.scss +++ b/app/assets/stylesheets/bootstrap_migration.scss @@ -87,7 +87,8 @@ table { display: none; } -.dropdown-toggle::after { +.dropdown-toggle::after, +.dropright .dropdown-menu-toggle::after { // Remove bootstrap's dropdown caret display: none; } @@ -148,8 +149,14 @@ table { } } -.nav-tabs .nav-link { - border: 0; +.nav-tabs { + .nav-link { + border: 0; + } + + .nav-item { + margin-bottom: 0; + } } pre code { diff --git a/app/assets/stylesheets/errors.scss b/app/assets/stylesheets/errors.scss new file mode 100644 index 00000000000..658e0ff638e --- /dev/null +++ b/app/assets/stylesheets/errors.scss @@ -0,0 +1,120 @@ +/* + * This is a minimal stylesheet, meant to be used for error pages. + */ +@import 'framework/variables'; +@import '../../../node_modules/bootstrap/scss/functions'; +@import '../../../node_modules/bootstrap/scss/variables'; +@import '../../../node_modules/bootstrap/scss/mixins'; +@import '../../../node_modules/bootstrap/scss/reboot'; +@import '../../../node_modules/bootstrap/scss/buttons'; +@import '../../../node_modules/bootstrap/scss/forms'; + +$body-color: #666; +$header-color: #456; + +body { + color: $body-color; + text-align: center; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + margin: auto; + font-size: 14px; +} + +h1 { + font-size: 56px; + line-height: 100px; + font-weight: 400; + color: $header-color; +} + +h2 { + font-size: 24px; + color: $body-color; + line-height: 1.5em; +} + +h3 { + color: $header-color; + font-size: 20px; + font-weight: 400; + line-height: 28px; +} + +img { + max-width: 80vw; + display: block; + margin: 40px auto; +} + +a { + text-decoration: none; + color: $blue-600; +} + +.page-container { + margin: auto 20px; +} + +.container { + margin: auto; + max-width: 600px; + border-bottom: 1px solid $border-color; + padding-bottom: 1em; +} + +.action-container { + padding: 0.5em 0; +} + +.form-inline-flex { + display: flex; + flex-wrap: wrap; + + button { + display: block; + width: 100%; + } + + .field { + display: block; + width: 100%; + margin-bottom: 1em; + } + + @include media-breakpoint-up(sm) { + flex-wrap: nowrap; + + button { + width: auto; + } + + .field { + margin-bottom: 0; + margin-right: 0.5em; + } + } +} + +.error-nav { + padding: 0; + text-align: center; + + li { + display: block; + padding-bottom: 1em; + } + + @include media-breakpoint-up(sm) { + + li { + display: inline-block; + padding-bottom: 0; + + &:not(:first-child)::before { + content: '\00B7'; + display: inline-block; + padding: 0 1em; + } + } + } +} diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss index d6ae8cbb416..3bbb50117bc 100644 --- a/app/assets/stylesheets/framework/gitlab_theme.scss +++ b/app/assets/stylesheets/framework/gitlab_theme.scss @@ -169,11 +169,14 @@ color: $color-800; } - .nav-links li a.active { - border-bottom: 2px solid $color-500; + .nav-links li { + &.active a, + a.active { + border-bottom: 2px solid $color-500; - .badge.badge-pill { - font-weight: $gl-font-weight-bold; + .badge.badge-pill { + font-weight: $gl-font-weight-bold; + } } } diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 0ea0b65b95f..d76cf8f8182 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -19,14 +19,23 @@ width: auto; display: inline-block; overflow-x: auto; - border-left: 0; - border-right: 0; - border-bottom: 0; + border: 0; + border-color: $md-area-border; @supports(width: fit-content) { display: block; width: fit-content; } + + tr { + th { + border-bottom: solid 2px $md-area-border; + } + + td { + border-color: $md-area-border; + } + } } /* diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index 667661d8b5c..ed5a1c91d8f 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -74,12 +74,6 @@ body.modal-open { } } -@include media-breakpoint-up(lg) { - .modal-full { - width: 98%; - } -} - .modal { background-color: $black-transparent; z-index: 2100; diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss index c3c64adf3da..847fc8c0792 100644 --- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss +++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss @@ -31,14 +31,15 @@ color: $black; } } + } - &.active { - color: $black; - font-weight: $gl-font-weight-bold; + &.active a, + a.active { + color: $black; + font-weight: $gl-font-weight-bold; - .badge.badge-pill { - color: $black; - } + .badge.badge-pill { + color: $black; } } } diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss index f80e9b1014b..7152ef9bcfd 100644 --- a/app/assets/stylesheets/framework/snippets.scss +++ b/app/assets/stylesheets/framework/snippets.scss @@ -49,26 +49,11 @@ margin-top: 15px; } -.snippet-embed-input { - height: 35px; -} - .embed-snippet { padding-right: 0; padding-top: $gl-padding; - .form-control { - cursor: auto; - width: 101%; - margin-left: -1px; - } - .embed-toggle-list li button { padding: 8px 40px; } - - .embed-toggle, - .snippet-clipboard-btn { - height: 35px; - } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 89b61530ddb..946223cfff0 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -373,6 +373,7 @@ $dropdown-chevron-size: 10px; $dropdown-toggle-active-border-color: darken($border-color, 14%); $dropdown-item-hover-bg: $gray-darker; $dropdown-fade-mask-height: 32px; +$dropdown-member-form-control-width: 163px; /* * Filtered Search diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss index de2b5701e2d..9914555d309 100644 --- a/app/assets/stylesheets/pages/members.scss +++ b/app/assets/stylesheets/pages/members.scss @@ -36,13 +36,12 @@ } } - .form-horizontal { - margin-top: 20px; + .form-group { + margin-bottom: 0; - @include media-breakpoint-up(sm) { - display: -webkit-flex; - display: flex; - margin-top: 3px; + @include media-breakpoint-down(sm) { + display: block; + margin-left: 5px; } } @@ -62,10 +61,15 @@ } .member-form-control { - @include media-breakpoint-down(xs) { - padding-bottom: 5px; + @include media-breakpoint-down(sm) { + width: $dropdown-member-form-control-width; margin-left: 0; + padding-bottom: 5px; + } + + @include media-breakpoint-down(xs) { margin-right: 0; + width: auto; } } @@ -207,10 +211,6 @@ align-self: flex-start; } - .form-horizontal ~ .btn { - margin-right: 0; - } - @include media-breakpoint-down(xs) { display: block; @@ -220,6 +220,12 @@ display: block; } + .controls > .btn:last-child { + margin-left: 5px; + margin-right: 5px; + width: auto; + } + .form-control { width: 100%; } @@ -232,10 +238,6 @@ .member-controls { margin-top: 5px; } - - .form-horizontal { - margin-top: 10px; - } } } @@ -259,10 +261,6 @@ margin-top: 0; } - .form-horizontal { - display: block; - } - .member-form-control { margin: 5px 0; } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 2843d70c645..db8a8cdc0d2 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -146,14 +146,15 @@ class ApplicationController < ActionController::Base end def render_403 - head :forbidden + respond_to do |format| + format.any { head :forbidden } + format.html { render "errors/access_denied", layout: "errors", status: 403 } + end end def render_404 respond_to do |format| - format.html do - render file: Rails.root.join("public", "404"), layout: false, status: "404" - end + format.html { render "errors/not_found", layout: "errors", status: 404 } # Prevent the Rails CSRF protector from thinking a missing .js file is a JavaScript file format.js { render json: '', status: :not_found, content_type: 'application/json' } format.any { head :not_found } diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb index 3b11a373368..b6eb7d292fc 100644 --- a/app/controllers/concerns/issues_action.rb +++ b/app/controllers/concerns/issues_action.rb @@ -17,10 +17,23 @@ module IssuesAction end # rubocop:enable Gitlab/ModuleWithInstanceVariables + # rubocop:disable Gitlab/ModuleWithInstanceVariables + def issues_calendar + @issues = issuables_collection + .non_archived + .with_due_date + .limit(100) + + respond_to do |format| + format.ics { response.headers['Content-Disposition'] = 'inline' } + end + end + # rubocop:enable Gitlab/ModuleWithInstanceVariables + private def finder_type (super if defined?(super)) || - (IssuesFinder if action_name == 'issues') + (IssuesFinder if %w(issues issues_calendar).include?(action_name)) end end diff --git a/app/controllers/groups/shared_projects_controller.rb b/app/controllers/groups/shared_projects_controller.rb new file mode 100644 index 00000000000..f2f835767e0 --- /dev/null +++ b/app/controllers/groups/shared_projects_controller.rb @@ -0,0 +1,31 @@ +module Groups + class SharedProjectsController < Groups::ApplicationController + respond_to :json + before_action :group + skip_cross_project_access_check :index + + def index + shared_projects = GroupProjectsFinder.new( + group: group, + current_user: current_user, + params: finder_params, + options: { only_shared: true } + ).execute + serializer = GroupChildSerializer.new(current_user: current_user) + .with_pagination(request, response) + + render json: serializer.represent(shared_projects) + end + + private + + def finder_params + @finder_params ||= begin + # Make the `search` param consistent for the frontend, + # which will be using `filter`. + params[:search] ||= params[:filter] if params[:filter] + params.permit(:sort, :search) + end + end + end +end diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 9f5ad23a20f..074db361949 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -34,12 +34,12 @@ class ProfilesController < Profiles::ApplicationController redirect_to profile_personal_access_tokens_path end - def reset_rss_token + def reset_feed_token Users::UpdateService.new(current_user, user: @user).execute! do |user| - user.reset_rss_token! + user.reset_feed_token! end - flash[:notice] = "RSS token was successfully reset" + flash[:notice] = 'Feed token was successfully reset' redirect_to profile_personal_access_tokens_path end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index d69015c8665..35c36c725e2 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -10,8 +10,8 @@ class Projects::IssuesController < Projects::ApplicationController before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update] before_action :check_issues_available! - before_action :issue, except: [:index, :new, :create, :bulk_update] - before_action :set_issuables_index, only: [:index] + before_action :issue, except: [:index, :calendar, :new, :create, :bulk_update] + before_action :set_issuables_index, only: [:index, :calendar] # Allow write(create) issue before_action :authorize_create_issue!, only: [:new, :create] @@ -39,6 +39,17 @@ class Projects::IssuesController < Projects::ApplicationController end end + def calendar + @issues = @issuables + .non_archived + .with_due_date + .limit(100) + + respond_to do |format| + format.ics { response.headers['Content-Disposition'] = 'inline' } + end + end + def new params[:issue] ||= ActionController::Parameters.new( assignee_ids: "" diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb index 67d4ea2ca8f..29632bef7e5 100644 --- a/app/controllers/projects/merge_requests/application_controller.rb +++ b/app/controllers/projects/merge_requests/application_controller.rb @@ -24,6 +24,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont :source_branch, :source_project_id, :state_event, + :squash, :target_branch, :target_project_id, :task_num, diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 62b739918e6..ecea6e1b2bf 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -253,7 +253,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo end def merge_params_attributes - [:should_remove_source_branch, :commit_message] + [:should_remove_source_branch, :commit_message, :squash] end def merge_when_pipeline_succeeds_active? @@ -282,7 +282,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo return :sha_mismatch if params[:sha] != @merge_request.diff_head_sha - @merge_request.update(merge_error: nil) + @merge_request.update(merge_error: nil, squash: merge_params.fetch(:squash, false)) if params[:merge_when_pipeline_succeeds].present? return :failed unless @merge_request.actual_head_pipeline @@ -296,14 +296,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo elsif @merge_request.actual_head_pipeline.success? # This can be triggered when a user clicks the auto merge button while # the tests finish at about the same time - @merge_request.merge_async(current_user.id, params) + @merge_request.merge_async(current_user.id, merge_params) :success else :failed end else - @merge_request.merge_async(current_user.id, params) + @merge_request.merge_async(current_user.id, merge_params) :success end diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index 1787b4899cd..3626670d141 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -75,6 +75,8 @@ class IssuesFinder < IssuableFinder items = items.due_between(Date.today.beginning_of_week, Date.today.end_of_week) elsif filter_by_due_this_month? items = items.due_between(Date.today.beginning_of_month, Date.today.end_of_month) + elsif filter_by_due_next_month_and_previous_two_weeks? + items = items.due_between(Date.today - 2.weeks, (Date.today + 1.month).end_of_month) end end @@ -97,6 +99,10 @@ class IssuesFinder < IssuableFinder due_date? && params[:due_date] == Issue::DueThisMonth.name end + def filter_by_due_next_month_and_previous_two_weeks? + due_date? && params[:due_date] == Issue::DueNextMonthAndPreviousTwoWeeks.name + end + def due_date? params[:due_date].present? end diff --git a/app/helpers/calendar_helper.rb b/app/helpers/calendar_helper.rb new file mode 100644 index 00000000000..c54b91b0ce5 --- /dev/null +++ b/app/helpers/calendar_helper.rb @@ -0,0 +1,8 @@ +module CalendarHelper + def calendar_url_options + { format: :ics, + feed_token: current_user.try(:feed_token), + due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name, + sort: 'closest_future_date' } + end +end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index c19c5b9cc82..74251c260f0 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -97,8 +97,9 @@ module MergeRequestsHelper { merge_when_pipeline_succeeds: true, should_remove_source_branch: true, - sha: merge_request.diff_head_sha - }.merge(merge_params_ee(merge_request)) + sha: merge_request.diff_head_sha, + squash: merge_request.squash + } end def tab_link_for(merge_request, tab, options = {}, &block) @@ -149,8 +150,4 @@ module MergeRequestsHelper current_user.fork_of(project) end end - - def merge_params_ee(merge_request) - {} - end end diff --git a/app/helpers/rss_helper.rb b/app/helpers/rss_helper.rb index 9ac4df88dc3..7d4fa83a67a 100644 --- a/app/helpers/rss_helper.rb +++ b/app/helpers/rss_helper.rb @@ -1,5 +1,5 @@ module RssHelper def rss_url_options - { format: :atom, rss_token: current_user.try(:rss_token) } + { format: :atom, feed_token: current_user.try(:feed_token) } end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index b12f7a2c83f..3d58a14882f 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -360,17 +360,6 @@ class ApplicationSetting < ActiveRecord::Base Array(read_attribute(:repository_storages)) end - # DEPRECATED - # repository_storage is still required in the API. Remove in 9.0 - # Still used in API v3 - def repository_storage - repository_storages.first - end - - def repository_storage=(value) - self.repository_storages = [value] - end - def default_project_visibility=(level) super(Gitlab::VisibilityLevel.level_value(level)) end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index b45395343cc..44150b37708 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -97,8 +97,6 @@ module Issuable strip_attributes :title - after_save :ensure_metrics, unless: :imported? - # We want to use optimistic lock for cases when only title or description are involved # http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html def locking_enabled? diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb index b3fec99c816..1f7d78a2efe 100644 --- a/app/models/concerns/project_features_compatibility.rb +++ b/app/models/concerns/project_features_compatibility.rb @@ -1,4 +1,4 @@ -# Makes api V3 compatible with old project features permissions methods +# Makes api V4 compatible with old project features permissions methods # # After migrating issues_enabled merge_requests_enabled builds_enabled snippets_enabled and wiki_enabled # fields to a new table "project_features", support for the old fields is still needed in the API. diff --git a/app/models/issue.rb b/app/models/issue.rb index d0e8fe908e4..d136700836d 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -15,12 +15,13 @@ class Issue < ActiveRecord::Base ignore_column :assignee_id, :branch_name, :deleted_at - DueDateStruct = Struct.new(:title, :name).freeze - NoDueDate = DueDateStruct.new('No Due Date', '0').freeze - AnyDueDate = DueDateStruct.new('Any Due Date', '').freeze - Overdue = DueDateStruct.new('Overdue', 'overdue').freeze - DueThisWeek = DueDateStruct.new('Due This Week', 'week').freeze - DueThisMonth = DueDateStruct.new('Due This Month', 'month').freeze + DueDateStruct = Struct.new(:title, :name).freeze + NoDueDate = DueDateStruct.new('No Due Date', '0').freeze + AnyDueDate = DueDateStruct.new('Any Due Date', '').freeze + Overdue = DueDateStruct.new('Overdue', 'overdue').freeze + DueThisWeek = DueDateStruct.new('Due This Week', 'week').freeze + DueThisMonth = DueDateStruct.new('Due This Month', 'month').freeze + DueNextMonthAndPreviousTwoWeeks = DueDateStruct.new('Due Next Month And Previous Two Weeks', 'next_month_and_previous_two_weeks').freeze belongs_to :project belongs_to :moved_to, class_name: 'Issue' @@ -47,6 +48,7 @@ class Issue < ActiveRecord::Base scope :unassigned, -> { where('NOT EXISTS (SELECT TRUE FROM issue_assignees WHERE issue_id = issues.id)') } scope :assigned_to, ->(u) { where('EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = ? AND issue_id = issues.id)', u.id)} + scope :with_due_date, -> { where('due_date IS NOT NULL') } scope :without_due_date, -> { where(due_date: nil) } scope :due_before, ->(date) { where('issues.due_date < ?', date) } scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) } @@ -54,12 +56,14 @@ class Issue < ActiveRecord::Base scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') } scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') } + scope :order_closest_future_date, -> { reorder('CASE WHEN due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - due_date) ASC') } scope :preload_associations, -> { preload(:labels, project: :namespace) } scope :public_only, -> { where(confidential: false) } after_save :expire_etag_cache + after_save :ensure_metrics, unless: :imported? attr_spammable :title, spam_title: true attr_spammable :description, spam_description: true @@ -120,6 +124,7 @@ class Issue < ActiveRecord::Base def self.sort_by_attribute(method, excluded_labels: []) case method.to_s + when 'closest_future_date' then order_closest_future_date when 'due_date' then order_due_date_asc when 'due_date_asc' then order_due_date_asc when 'due_date_desc' then order_due_date_desc diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index a00b8d4e2c1..4c1628d2bdb 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -59,6 +59,7 @@ class MergeRequest < ActiveRecord::Base after_create :ensure_merge_request_diff, unless: :importing? after_update :clear_memoized_shas after_update :reload_diff_if_branch_changed + after_save :ensure_metrics # When this attribute is true some MR validation is ignored # It allows us to close or modify broken merge requests @@ -1141,4 +1142,11 @@ class MergeRequest < ActiveRecord::Base maintainer_push_possible? && Ability.allowed?(user, :push_code, source_project) end + + def squash_in_progress? + # The source project can be deleted + return false unless source_project + + source_project.repository.squash_in_progress?(id) + end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 0e1bf11d7c0..82cf47ba04e 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -957,6 +957,22 @@ class Repository remote_branch: merge_request.target_branch) end + def blob_data_at(sha, path) + blob = blob_at(sha, path) + return unless blob + + blob.load_all_data! + blob.data + end + + def squash(user, merge_request) + raw.squash(user, merge_request.id, branch: merge_request.target_branch, + start_sha: merge_request.diff_start_sha, + end_sha: merge_request.diff_head_sha, + author: merge_request.author, + message: merge_request.title) + end + private # TODO Generice finder, later split this on finders by Ref or Oid @@ -971,14 +987,6 @@ class Repository ::Commit.new(commit, @project) if commit end - def blob_data_at(sha, path) - blob = blob_at(sha, path) - return unless blob - - blob.load_all_data! - blob.data - end - def cache @cache ||= Gitlab::RepositoryCache.new(self) end diff --git a/app/models/service.rb b/app/models/service.rb index 831c2ea1141..1d259bcfec7 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -206,10 +206,11 @@ class Service < ActiveRecord::Base args.each do |arg| class_eval %{ def #{arg}? + # '!!' is used because nil or empty string is converted to nil if Gitlab.rails5? - !ActiveModel::Type::Boolean::FALSE_VALUES.include?(#{arg}) + !!ActiveRecord::Type::Boolean.new.cast(#{arg}) else - ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(#{arg}) + !!ActiveRecord::Type::Boolean.new.type_cast_from_database(#{arg}) end end } diff --git a/app/models/user.rb b/app/models/user.rb index 0a838d34054..e219ab800ad 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -26,7 +26,7 @@ class User < ActiveRecord::Base ignore_column :authentication_token add_authentication_token_field :incoming_email_token - add_authentication_token_field :rss_token + add_authentication_token_field :feed_token default_value_for :admin, false default_value_for(:external) { Gitlab::CurrentSettings.user_default_external } @@ -1167,11 +1167,11 @@ class User < ActiveRecord::Base save end - # each existing user needs to have an `rss_token`. + # each existing user needs to have an `feed_token`. # we do this on read since migrating all existing users is not a feasible # solution. - def rss_token - ensure_rss_token! + def feed_token + ensure_feed_token! end def sync_attribute?(attribute) diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb index c7f7aa836bd..9a7aaf4ef32 100644 --- a/app/presenters/commit_status_presenter.rb +++ b/app/presenters/commit_status_presenter.rb @@ -1,11 +1,10 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated CALLOUT_FAILURE_MESSAGES = { unknown_failure: 'There is an unknown failure, please try again', - script_failure: 'There has been a script failure. Check the job log for more information', api_failure: 'There has been an API failure, please try again', stuck_or_timeout_failure: 'There has been a timeout failure or the job got stuck. Check your timeout limits or try again', runner_system_failure: 'There has been a runner system failure, please try again', - missing_dependency_failure: 'There has been a missing dependency failure, check the job log for more information' + missing_dependency_failure: 'There has been a missing dependency failure' }.freeze presents :build diff --git a/app/serializers/group_child_entity.rb b/app/serializers/group_child_entity.rb index 15ec0f89bb2..ee150eefd9e 100644 --- a/app/serializers/group_child_entity.rb +++ b/app/serializers/group_child_entity.rb @@ -31,7 +31,7 @@ class GroupChildEntity < Grape::Entity end # Project only attributes - expose :star_count, + expose :star_count, :archived, if: lambda { |_instance, _options| project? } # Group only attributes diff --git a/app/serializers/job_entity.rb b/app/serializers/job_entity.rb index 3076fed1674..960e7291ae6 100644 --- a/app/serializers/job_entity.rb +++ b/app/serializers/job_entity.rb @@ -26,7 +26,7 @@ class JobEntity < Grape::Entity expose :created_at expose :updated_at expose :detailed_status, as: :status, with: StatusEntity - expose :callout_message, if: -> (*) { failed? } + expose :callout_message, if: -> (*) { failed? && !build.script_failure? } expose :recoverable, if: -> (*) { failed? } private diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb index d0165c148eb..141070aef45 100644 --- a/app/serializers/merge_request_widget_entity.rb +++ b/app/serializers/merge_request_widget_entity.rb @@ -10,6 +10,7 @@ class MergeRequestWidgetEntity < IssuableEntity expose :merge_when_pipeline_succeeds expose :source_branch expose :source_project_id + expose :squash expose :target_branch expose :target_project_id expose :allow_maintainer_to_push diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index ac70a99c2c5..5a961ac89e4 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -63,7 +63,7 @@ module Boards def without_board_labels(issues) return issues unless board_label_ids.any? - issues.where.not(issues_label_links.limit(1).arel.exists) + issues.where.not('EXISTS (?)', issues_label_links.limit(1)) end def issues_label_links @@ -71,10 +71,8 @@ module Boards end def with_list_label(issues) - issues.where( - LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id") - .where("label_links.label_id = ?", list.label_id).limit(1).arel.exists - ) + issues.where('EXISTS (?)', LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id") + .where("label_links.label_id = ?", list.label_id).limit(1)) end end end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 2209a60a840..126da891c78 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -34,6 +34,19 @@ module MergeRequests handle_merge_error(log_message: e.message, save_message_on_model: true) end + def source + return merge_request.diff_head_sha unless merge_request.squash + + squash_result = ::MergeRequests::SquashService.new(project, current_user, params).execute(merge_request) + + case squash_result[:status] + when :success + squash_result[:squash_sha] + when :error + raise ::MergeRequests::MergeService::MergeError, squash_result[:message] + end + end + private def error_check! @@ -116,9 +129,5 @@ module MergeRequests def merge_request_info merge_request.to_reference(full: true) end - - def source - @source ||= @merge_request.diff_head_sha - end end end diff --git a/app/services/merge_requests/squash_service.rb b/app/services/merge_requests/squash_service.rb new file mode 100644 index 00000000000..a40fb2786bd --- /dev/null +++ b/app/services/merge_requests/squash_service.rb @@ -0,0 +1,28 @@ +module MergeRequests + class SquashService < MergeRequests::WorkingCopyBaseService + def execute(merge_request) + @merge_request = merge_request + @repository = target_project.repository + + squash || error('Failed to squash. Should be done manually.') + end + + def squash + if merge_request.commits_count < 2 + return success(squash_sha: merge_request.diff_head_sha) + end + + if merge_request.squash_in_progress? + return error('Squash task canceled: another squash is already in progress.') + end + + squash_sha = repository.squash(current_user, merge_request) + + success(squash_sha: squash_sha) + rescue => e + log_error("Failed to squash merge request #{merge_request.to_reference(full: true)}:") + log_error(e.message) + false + end + end +end diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb index 3fd27d9acdc..5bdca26a584 100644 --- a/app/uploaders/object_storage.rb +++ b/app/uploaders/object_storage.rb @@ -11,6 +11,7 @@ module ObjectStorage ObjectStorageUnavailable = Class.new(StandardError) DIRECT_UPLOAD_TIMEOUT = 4.hours + DIRECT_UPLOAD_EXPIRE_OFFSET = 15.minutes TMP_UPLOAD_PATH = 'tmp/uploads'.freeze module Store @@ -174,11 +175,12 @@ module ObjectStorage id = [CarrierWave.generate_cache_id, SecureRandom.hex].join('-') upload_path = File.join(TMP_UPLOAD_PATH, id) connection = ::Fog::Storage.new(self.object_store_credentials) - expire_at = Time.now + DIRECT_UPLOAD_TIMEOUT + expire_at = Time.now + DIRECT_UPLOAD_TIMEOUT + DIRECT_UPLOAD_EXPIRE_OFFSET options = { 'Content-Type' => 'application/octet-stream' } { ID: id, + Timeout: DIRECT_UPLOAD_TIMEOUT, GetURL: connection.get_object_url(remote_store_path, upload_path, expire_at), DeleteURL: connection.delete_object_url(remote_store_path, upload_path, expire_at), StoreURL: connection.put_object_url(remote_store_path, upload_path, expire_at, options) diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml index f40dd347eb3..07f9ea0865b 100644 --- a/app/views/admin/application_settings/_account_and_limit.html.haml +++ b/app/views/admin/application_settings/_account_and_limit.html.haml @@ -5,8 +5,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :gravatar_enabled do - = f.check_box :gravatar_enabled + = f.check_box :gravatar_enabled, class: 'form-check-input' + = f.label :gravatar_enabled, class: 'form-check-label' do Gravatar enabled .form-group.row = f.label :default_projects_limit, class: 'col-form-label col-sm-2' @@ -25,15 +25,15 @@ = f.label :user_oauth_applications, 'User OAuth applications', class: 'col-form-label col-sm-2' .col-sm-10 .form-check - = f.label :user_oauth_applications do - = f.check_box :user_oauth_applications + = f.check_box :user_oauth_applications, class: 'form-check-input' + = f.label :user_oauth_applications, class: 'form-check-label' do Allow users to register any application to use GitLab as an OAuth provider .form-group.row = f.label :user_default_external, 'New users set to external', class: 'col-form-label col-sm-2' .col-sm-10 .form-check - = f.label :user_default_external do - = f.check_box :user_default_external + = f.check_box :user_default_external, class: 'form-check-input' + = f.label :user_default_external, class: 'form-check-label' do Newly registered users will by default be external = f.submit 'Save changes', class: 'btn btn-success' diff --git a/app/views/admin/application_settings/_background_jobs.html.haml b/app/views/admin/application_settings/_background_jobs.html.haml index da7248337d2..fc5df02242a 100644 --- a/app/views/admin/application_settings/_background_jobs.html.haml +++ b/app/views/admin/application_settings/_background_jobs.html.haml @@ -9,8 +9,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :sidekiq_throttling_enabled do - = f.check_box :sidekiq_throttling_enabled + = f.check_box :sidekiq_throttling_enabled, class: 'form-check-input' + = f.label :sidekiq_throttling_enabled, class: 'form-check-label' do Enable Sidekiq Job Throttling .form-text.text-muted Limit the amount of resources slow running jobs are assigned. diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml index 6a06271de2a..233821818e6 100644 --- a/app/views/admin/application_settings/_ci_cd.html.haml +++ b/app/views/admin/application_settings/_ci_cd.html.haml @@ -5,8 +5,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :auto_devops_enabled do - = f.check_box :auto_devops_enabled + = f.check_box :auto_devops_enabled, class: 'form-check-input' + = f.label :auto_devops_enabled, class: 'form-check-label' do Enabled Auto DevOps for projects by default .form-text.text-muted It will automatically build, test, and deploy applications based on a predefined CI/CD configuration @@ -20,8 +20,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :shared_runners_enabled do - = f.check_box :shared_runners_enabled + = f.check_box :shared_runners_enabled, class: 'form-check-input' + = f.label :shared_runners_enabled, class: 'form-check-label' do Enable shared runners for new projects .form-group.row = f.label :shared_runners_text, class: 'col-form-label col-sm-2' diff --git a/app/views/admin/application_settings/_email.html.haml b/app/views/admin/application_settings/_email.html.haml index 7443f02ad15..01be5878a60 100644 --- a/app/views/admin/application_settings/_email.html.haml +++ b/app/views/admin/application_settings/_email.html.haml @@ -5,8 +5,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :email_author_in_body do - = f.check_box :email_author_in_body + = f.check_box :email_author_in_body, class: 'form-check-input' + = f.label :email_author_in_body, class: 'form-check-label' do Include author name in notification email body .form-text.text-muted Some email servers do not support overriding the email sender name. @@ -15,8 +15,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :html_emails_enabled do - = f.check_box :html_emails_enabled + = f.check_box :html_emails_enabled, class: 'form-check-input' + = f.label :html_emails_enabled, class: 'form-check-label' do Enable HTML emails .form-text.text-muted By default GitLab sends emails in HTML and plain text formats so mail diff --git a/app/views/admin/application_settings/_help_page.html.haml b/app/views/admin/application_settings/_help_page.html.haml index 97d397788c6..1f6c52d8b1a 100644 --- a/app/views/admin/application_settings/_help_page.html.haml +++ b/app/views/admin/application_settings/_help_page.html.haml @@ -10,8 +10,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :help_page_hide_commercial_content do - = f.check_box :help_page_hide_commercial_content + = f.check_box :help_page_hide_commercial_content, class: 'form-check-input' + = f.label :help_page_hide_commercial_content, class: 'form-check-label' do Hide marketing-related entries from help .form-group.row = f.label :help_page_support_url, 'Support page URL', class: 'col-form-label col-sm-2' diff --git a/app/views/admin/application_settings/_influx.html.haml b/app/views/admin/application_settings/_influx.html.haml index e5d699f9a2f..b40a714ed8f 100644 --- a/app/views/admin/application_settings/_influx.html.haml +++ b/app/views/admin/application_settings/_influx.html.haml @@ -11,8 +11,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :metrics_enabled do - = f.check_box :metrics_enabled + = f.check_box :metrics_enabled, class: 'form-check-input' + = f.label :metrics_enabled, class: 'form-check-label' do Enable InfluxDB Metrics .form-group.row = f.label :metrics_host, 'InfluxDB host', class: 'col-form-label col-sm-2' diff --git a/app/views/admin/application_settings/_ip_limits.html.haml b/app/views/admin/application_settings/_ip_limits.html.haml index 539ff9b5168..320dd52ffc2 100644 --- a/app/views/admin/application_settings/_ip_limits.html.haml +++ b/app/views/admin/application_settings/_ip_limits.html.haml @@ -5,8 +5,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :throttle_unauthenticated_enabled do - = f.check_box :throttle_unauthenticated_enabled + = f.check_box :throttle_unauthenticated_enabled, class: 'form-check-input' + = f.label :throttle_unauthenticated_enabled, class: 'form-check-label' do Enable unauthenticated request rate limit %span.form-text.text-muted Helps reduce request volume (e.g. from crawlers or abusive bots) @@ -21,8 +21,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :throttle_authenticated_api_enabled do - = f.check_box :throttle_authenticated_api_enabled + = f.check_box :throttle_authenticated_api_enabled, class: 'form-check-input' + = f.label :throttle_authenticated_api_enabled, class: 'form-check-label' do Enable authenticated API request rate limit %span.form-text.text-muted Helps reduce request volume (e.g. from crawlers or abusive bots) @@ -37,8 +37,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :throttle_authenticated_web_enabled do - = f.check_box :throttle_authenticated_web_enabled + = f.check_box :throttle_authenticated_web_enabled, class: 'form-check-input' + = f.label :throttle_authenticated_web_enabled, class: 'form-check-label' do Enable authenticated web request rate limit %span.form-text.text-muted Helps reduce request volume (e.g. from crawlers or abusive bots) diff --git a/app/views/admin/application_settings/_koding.html.haml b/app/views/admin/application_settings/_koding.html.haml index 0532f49fd5b..341c7641fcc 100644 --- a/app/views/admin/application_settings/_koding.html.haml +++ b/app/views/admin/application_settings/_koding.html.haml @@ -5,8 +5,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :koding_enabled do - = f.check_box :koding_enabled + = f.check_box :koding_enabled, class: 'form-check-input' + = f.label :koding_enabled, class: 'form-check-label' do Enable Koding .form-text.text-muted Koding integration has been deprecated since GitLab 10.0. If you disable your Koding integration, you will not be able to enable it again. diff --git a/app/views/admin/application_settings/_logging.html.haml b/app/views/admin/application_settings/_logging.html.haml index 4341801b045..f5c1e126c70 100644 --- a/app/views/admin/application_settings/_logging.html.haml +++ b/app/views/admin/application_settings/_logging.html.haml @@ -5,8 +5,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :sentry_enabled do - = f.check_box :sentry_enabled + = f.check_box :sentry_enabled, class: 'form-check-input' + = f.label :sentry_enabled, class: 'form-check-label' do Enable Sentry .form-text.text-muted %p This setting requires a restart to take effect. @@ -21,8 +21,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :clientside_sentry_enabled do - = f.check_box :clientside_sentry_enabled + = f.check_box :clientside_sentry_enabled, class: 'form-check-input' + = f.label :clientside_sentry_enabled, class: 'form-check-label' do Enable Clientside Sentry .form-text.text-muted Sentry can also be used for reporting and logging clientside exceptions. diff --git a/app/views/admin/application_settings/_outbound.html.haml b/app/views/admin/application_settings/_outbound.html.haml index 176cdfe9e1a..5dadb7b814b 100644 --- a/app/views/admin/application_settings/_outbound.html.haml +++ b/app/views/admin/application_settings/_outbound.html.haml @@ -5,8 +5,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :allow_local_requests_from_hooks_and_services do - = f.check_box :allow_local_requests_from_hooks_and_services + = f.check_box :allow_local_requests_from_hooks_and_services, class: 'form-check-input' + = f.label :allow_local_requests_from_hooks_and_services, class: 'form-check-label' do Allow requests to the local network from hooks and services = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_pages.html.haml b/app/views/admin/application_settings/_pages.html.haml index 0100ef038e2..f1889c3105f 100644 --- a/app/views/admin/application_settings/_pages.html.haml +++ b/app/views/admin/application_settings/_pages.html.haml @@ -10,8 +10,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :pages_domain_verification_enabled do - = f.check_box :pages_domain_verification_enabled + = f.check_box :pages_domain_verification_enabled, class: 'form-check-input' + = f.label :pages_domain_verification_enabled, class: 'form-check-label' do Require users to prove ownership of custom domains .form-text.text-muted Domain verification is an essential security measure for public GitLab diff --git a/app/views/admin/application_settings/_performance.html.haml b/app/views/admin/application_settings/_performance.html.haml index 07d3a8b7cdf..57c22ce563f 100644 --- a/app/views/admin/application_settings/_performance.html.haml +++ b/app/views/admin/application_settings/_performance.html.haml @@ -5,8 +5,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :authorized_keys_enabled do - = f.check_box :authorized_keys_enabled + = f.check_box :authorized_keys_enabled, class: 'form-check-input' + = f.label :authorized_keys_enabled, class: 'form-check-label' do Write to "authorized_keys" file .form-text.text-muted By default, we write to the "authorized_keys" file to support Git diff --git a/app/views/admin/application_settings/_performance_bar.html.haml b/app/views/admin/application_settings/_performance_bar.html.haml index 030e8610b47..ed4de2234f7 100644 --- a/app/views/admin/application_settings/_performance_bar.html.haml +++ b/app/views/admin/application_settings/_performance_bar.html.haml @@ -5,8 +5,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :performance_bar_enabled do - = f.check_box :performance_bar_enabled + = f.check_box :performance_bar_enabled, class: 'form-check-input' + = f.label :performance_bar_enabled, class: 'form-check-label' do Enable the Performance Bar .form-group.row = f.label :performance_bar_allowed_group_path, 'Allowed group', class: 'col-form-label col-sm-2' diff --git a/app/views/admin/application_settings/_plantuml.html.haml b/app/views/admin/application_settings/_plantuml.html.haml index ee6d1d1a888..e0dc058762e 100644 --- a/app/views/admin/application_settings/_plantuml.html.haml +++ b/app/views/admin/application_settings/_plantuml.html.haml @@ -5,8 +5,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :plantuml_enabled do - = f.check_box :plantuml_enabled + = f.check_box :plantuml_enabled, class: 'form-check-input' + = f.label :plantuml_enabled, class: 'form-check-label' do Enable PlantUML .form-group.row = f.label :plantuml_url, 'PlantUML URL', class: 'col-form-label col-sm-2' diff --git a/app/views/admin/application_settings/_prometheus.html.haml b/app/views/admin/application_settings/_prometheus.html.haml index 8c95597e787..d3c3656e96a 100644 --- a/app/views/admin/application_settings/_prometheus.html.haml +++ b/app/views/admin/application_settings/_prometheus.html.haml @@ -14,8 +14,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :prometheus_metrics_enabled do - = f.check_box :prometheus_metrics_enabled + = f.check_box :prometheus_metrics_enabled, class: 'form-check-input' + = f.label :prometheus_metrics_enabled, class: 'form-check-label' do Enable Prometheus Metrics - unless Gitlab::Metrics.metrics_folder_present? .form-text.text-muted diff --git a/app/views/admin/application_settings/_repository_check.html.haml b/app/views/admin/application_settings/_repository_check.html.haml index 739ed359e16..1311f17ecda 100644 --- a/app/views/admin/application_settings/_repository_check.html.haml +++ b/app/views/admin/application_settings/_repository_check.html.haml @@ -7,8 +7,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :repository_checks_enabled do - = f.check_box :repository_checks_enabled + = f.check_box :repository_checks_enabled, class: 'form-check-input' + = f.label :repository_checks_enabled, class: 'form-check-label' do Enable Repository Checks .form-text.text-muted GitLab will periodically run @@ -25,8 +25,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :housekeeping_enabled do - = f.check_box :housekeeping_enabled + = f.check_box :housekeeping_enabled, class: 'form-check-input' + = f.label :housekeeping_enabled, class: 'form-check-label' do Enable automatic repository housekeeping (git repack, git gc) .form-text.text-muted If you keep automatic housekeeping disabled for a long time Git @@ -34,8 +34,8 @@ repositories will use more disk space. We recommend to always leave this enabled. .form-check - = f.label :housekeeping_bitmaps_enabled do - = f.check_box :housekeeping_bitmaps_enabled + = f.check_box :housekeeping_bitmaps_enabled, class: 'form-check-input' + = f.label :housekeeping_bitmaps_enabled, class: 'form-check-label' do Enable Git pack file bitmap creation .form-text.text-muted Creating pack file bitmaps makes housekeeping take a little longer but diff --git a/app/views/admin/application_settings/_repository_mirrors_form.html.haml b/app/views/admin/application_settings/_repository_mirrors_form.html.haml index edd8e5e9eb8..187c6c28bb1 100644 --- a/app/views/admin/application_settings/_repository_mirrors_form.html.haml +++ b/app/views/admin/application_settings/_repository_mirrors_form.html.haml @@ -1,13 +1,13 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path do |f| = form_errors(@application_setting) %fieldset - .form-group - = f.label :mirror_available, 'Enable mirror configuration', class: 'control-label col-sm-2' - .col-sm-10 + .form-group.row + = f.label :mirror_available, 'Enable mirror configuration', class: 'control-label col-sm-4' + .col-sm-8 .form-check - = f.label :mirror_available do - = f.check_box :mirror_available + = f.check_box :mirror_available, class: 'form-check-input' + = f.label :mirror_available, class: 'form-check-label' do Allow mirrors to be setup for projects %span.form-text.text-muted If disabled, only admins will be able to setup mirrors in projects. diff --git a/app/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml index be85210934c..89d2c114b22 100644 --- a/app/views/admin/application_settings/_repository_storage.html.haml +++ b/app/views/admin/application_settings/_repository_storage.html.haml @@ -6,8 +6,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :hashed_storage_enabled do - = f.check_box :hashed_storage_enabled + = f.check_box :hashed_storage_enabled, class: 'form-check-input' + = f.label :hashed_storage_enabled, class: 'form-check-label' do Create new projects using hashed storage paths .form-text.text-muted Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml index 83a30504222..2ba26158162 100644 --- a/app/views/admin/application_settings/_signin.html.haml +++ b/app/views/admin/application_settings/_signin.html.haml @@ -5,16 +5,16 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :password_authentication_enabled_for_web do - = f.check_box :password_authentication_enabled_for_web + = f.check_box :password_authentication_enabled_for_web, class: 'form-check-input' + = f.label :password_authentication_enabled_for_web, class: 'form-check-label' do Password authentication enabled for web interface .form-text.text-muted When disabled, an external authentication provider must be used. .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :password_authentication_enabled_for_git do - = f.check_box :password_authentication_enabled_for_git + = f.check_box :password_authentication_enabled_for_git, class: 'form-check-input' + = f.label :password_authentication_enabled_for_git, class: 'form-check-label' do Password authentication enabled for Git over HTTP(S) .form-text.text-muted When disabled, a Personal Access Token @@ -33,8 +33,8 @@ = f.label :two_factor_authentication, 'Two-factor authentication', class: 'col-form-label col-sm-2' .col-sm-10 .form-check - = f.label :require_two_factor_authentication do - = f.check_box :require_two_factor_authentication + = f.check_box :require_two_factor_authentication, class: 'form-check-input' + = f.label :require_two_factor_authentication, class: 'form-check-label' do Require all users to setup Two-factor authentication .form-group.row = f.label :two_factor_authentication, 'Two-factor grace period (hours)', class: 'col-form-label col-sm-2' diff --git a/app/views/admin/application_settings/_signup.html.haml b/app/views/admin/application_settings/_signup.html.haml index 50c455f8686..279f96389e9 100644 --- a/app/views/admin/application_settings/_signup.html.haml +++ b/app/views/admin/application_settings/_signup.html.haml @@ -5,14 +5,14 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :signup_enabled do - = f.check_box :signup_enabled + = f.check_box :signup_enabled, class: 'form-check-input' + = f.label :signup_enabled, class: 'form-check-label' do Sign-up enabled .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :send_user_confirmation_email do - = f.check_box :send_user_confirmation_email + = f.check_box :send_user_confirmation_email, class: 'form-check-input' + = f.label :send_user_confirmation_email, class: 'form-check-label' do Send confirmation email on sign-up .form-group.row = f.label :domain_whitelist, 'Whitelisted domains for sign-ups', class: 'col-form-label col-sm-2' @@ -23,19 +23,19 @@ = f.label :domain_blacklist_enabled, 'Domain Blacklist', class: 'col-form-label col-sm-2' .col-sm-10 .form-check - = f.label :domain_blacklist_enabled do - = f.check_box :domain_blacklist_enabled + = f.check_box :domain_blacklist_enabled, class: 'form-check-input' + = f.label :domain_blacklist_enabled, class: 'form-check-label' do Enable domain blacklist for sign ups .form-group.row .offset-sm-2.col-sm-10 .form-check - = label_tag :blacklist_type_file do - = radio_button_tag :blacklist_type, :file + = radio_button_tag :blacklist_type, :file, class: 'form-check-input' + = label_tag :blacklist_type_file, class: 'form-check-label' do .option-title Upload blacklist file .form-check - = label_tag :blacklist_type_raw do - = radio_button_tag :blacklist_type, :raw, @application_setting.domain_blacklist.present? || @application_setting.domain_blacklist.blank? + = radio_button_tag :blacklist_type, :raw, @application_setting.domain_blacklist.present? || @application_setting.domain_blacklist.blank?, class: 'form-check-input' + = label_tag :blacklist_type_raw, class: 'form-check-label' do .option-title Enter blacklist manually .form-group.row.blacklist-file diff --git a/app/views/admin/application_settings/_spam.html.haml b/app/views/admin/application_settings/_spam.html.haml index 58543a0359a..fb38e4ae922 100644 --- a/app/views/admin/application_settings/_spam.html.haml +++ b/app/views/admin/application_settings/_spam.html.haml @@ -5,8 +5,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :recaptcha_enabled do - = f.check_box :recaptcha_enabled + = f.check_box :recaptcha_enabled, class: 'form-check-input' + = f.label :recaptcha_enabled, class: 'form-check-label' do Enable reCAPTCHA %span.form-text.text-muted#recaptcha_help_block Helps prevent bots from creating accounts @@ -26,8 +26,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :akismet_enabled do - = f.check_box :akismet_enabled + = f.check_box :akismet_enabled, class: 'form-check-input' + = f.label :akismet_enabled, class: 'form-check-label' do Enable Akismet %span.form-text.text-muted#akismet_help_block Helps prevent bots from creating issues @@ -42,8 +42,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :unique_ips_limit_enabled do - = f.check_box :unique_ips_limit_enabled + = f.check_box :unique_ips_limit_enabled, class: 'form-check-input' + = f.label :unique_ips_limit_enabled, class: 'form-check-label' do Limit sign in from multiple ips %span.form-text.text-muted#unique_ip_help_block Helps prevent malicious users hide their activity diff --git a/app/views/admin/application_settings/_terms.html.haml b/app/views/admin/application_settings/_terms.html.haml index 44bf7b65a8e..257565ce193 100644 --- a/app/views/admin/application_settings/_terms.html.haml +++ b/app/views/admin/application_settings/_terms.html.haml @@ -1,16 +1,16 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path do |f| = form_errors(@application_setting) %fieldset - .form-group + .form-group.row .col-sm-12 .form-check - = f.label :enforce_terms do - = f.check_box :enforce_terms + = f.check_box :enforce_terms, class: 'form-check-input' + = f.label :enforce_terms, class: 'form-check-label' do = _("Require all users to accept Terms of Service when they access GitLab.") .form-text.text-muted = _("When enabled, users cannot use GitLab until the terms have been accepted.") - .form-group + .form-group.row .col-sm-12 = f.label :terms do = _("Terms of Service Agreement") diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml index 316c8b04dea..c110fd4d60d 100644 --- a/app/views/admin/application_settings/_usage.html.haml +++ b/app/views/admin/application_settings/_usage.html.haml @@ -5,8 +5,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :version_check_enabled do - = f.check_box :version_check_enabled + = f.check_box :version_check_enabled, class: 'form-check-input' + = f.label :version_check_enabled, class: 'form-check-label' do Enable version check .form-text.text-muted GitLab will inform you if a new version is available. @@ -16,8 +16,8 @@ .offset-sm-2.col-sm-10 - can_be_configured = @application_setting.usage_ping_can_be_configured? .form-check - = f.label :usage_ping_enabled do - = f.check_box :usage_ping_enabled, disabled: !can_be_configured + = f.check_box :usage_ping_enabled, disabled: !can_be_configured, class: 'form-check-input' + = f.label :usage_ping_enabled, class: 'form-check-label' do Enable usage ping .form-text.text-muted - if can_be_configured diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml index 0f2524047e3..2b4d3bab54d 100644 --- a/app/views/admin/application_settings/_visibility_and_access.html.haml +++ b/app/views/admin/application_settings/_visibility_and_access.html.haml @@ -46,8 +46,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :project_export_enabled do - = f.check_box :project_export_enabled + = f.check_box :project_export_enabled, class: 'form-check-input' + = f.label :project_export_enabled, class: 'form-check-label' do Project export enabled .form-group.row diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml index b5d79e1da13..7f14cddebd8 100644 --- a/app/views/admin/applications/_form.html.haml +++ b/app/views/admin/applications/_form.html.haml @@ -1,13 +1,13 @@ = form_for [:admin, @application], url: @url, html: {role: 'form'} do |f| = form_errors(application) - = content_tag :div, class: 'form-group' do + = content_tag :div, class: 'form-group row' do = f.label :name, class: 'col-sm-2 col-form-label' .col-sm-10 = f.text_field :name, class: 'form-control' = doorkeeper_errors_for application, :name - = content_tag :div, class: 'form-group' do + = content_tag :div, class: 'form-group row' do = f.label :redirect_uri, class: 'col-sm-2 col-form-label' .col-sm-10 = f.text_area :redirect_uri, class: 'form-control' @@ -20,7 +20,7 @@ %code= Doorkeeper.configuration.native_redirect_uri for local tests - = content_tag :div, class: 'form-group' do + = content_tag :div, class: 'form-group row' do = f.label :trusted, class: 'col-sm-2 col-form-label' .col-sm-10 = f.check_box :trusted diff --git a/app/views/admin/hooks/_form.html.haml b/app/views/admin/hooks/_form.html.haml index e54dbd20ef4..3abde755f0f 100644 --- a/app/views/admin/hooks/_form.html.haml +++ b/app/views/admin/hooks/_form.html.haml @@ -47,6 +47,6 @@ .form-group = form.label :enable_ssl_verification, 'SSL verification', class: 'label-light checkbox' .form-check - = form.label :enable_ssl_verification do - = form.check_box :enable_ssl_verification + = form.check_box :enable_ssl_verification, class: 'form-check-input' + = form.label :enable_ssl_verification, class: 'form-check-label' do %strong Enable SSL verification diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml index 4bf04dadf01..86a21e24ac9 100644 --- a/app/views/dashboard/issues.html.haml +++ b/app/views/dashboard/issues.html.haml @@ -7,8 +7,7 @@ .top-area = render 'shared/issuable/nav', type: :issues, display_count: !@no_filters_set .nav-controls - = link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe' do - = icon('rss') + = render 'shared/issuable/feed_buttons' = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues = render 'shared/issuable/filter', type: :issues diff --git a/app/views/dashboard/issues_calendar.ics.haml b/app/views/dashboard/issues_calendar.ics.haml new file mode 100644 index 00000000000..59573e5fecf --- /dev/null +++ b/app/views/dashboard/issues_calendar.ics.haml @@ -0,0 +1 @@ += render 'issues/issues_calendar', issues: @issues diff --git a/app/views/errors/_footer.html.haml b/app/views/errors/_footer.html.haml new file mode 100644 index 00000000000..e67a3a142f6 --- /dev/null +++ b/app/views/errors/_footer.html.haml @@ -0,0 +1,11 @@ +%nav + %ul.error-nav + %li + = link_to s_('Nav|Home'), root_path + %li + - if current_user + = link_to s_('Nav|Sign out and sign in with a different account'), destroy_user_session_path + - else + = link_to s_('Nav|Sign In / Register'), new_session_path(:user, redirect_to_referer: 'yes') + %li + = link_to s_('Nav|Help'), help_path diff --git a/app/views/errors/access_denied.html.haml b/app/views/errors/access_denied.html.haml index bf540439c79..227c7884915 100644 --- a/app/views/errors/access_denied.html.haml +++ b/app/views/errors/access_denied.html.haml @@ -1,15 +1,16 @@ - message = local_assigns.fetch(:message) - - content_for(:title, 'Access Denied') -%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') } - %h1 - 403 + += image_tag('illustrations/error-403.svg', alt: '403', lazy: false) .container - %h3 Access Denied - %hr + %h3 + = s_("403|You don't have the permission to access this page.") - if message %p = message - - else - %p You are not allowed to access this page. - %p Read more about project permissions #{link_to "here", help_page_path("user/permissions"), class: "vlink"} + %p + = s_('403|Please contact your GitLab administrator to get the permission.') + .action-container.js-go-back{ style: 'display: none' } + %a{ href: 'javascript:history.back()', class: 'btn btn-success' } + = s_('Go Back') += render "errors/footer" diff --git a/app/views/errors/not_found.html.haml b/app/views/errors/not_found.html.haml index a0b9a632e22..ae055f398ac 100644 --- a/app/views/errors/not_found.html.haml +++ b/app/views/errors/not_found.html.haml @@ -1,8 +1,15 @@ - content_for(:title, 'Not Found') -%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') } - %h1 - 404 += image_tag('illustrations/error-404.svg', alt: '404', lazy: false) .container - %h3 The resource you were looking for doesn't exist. - %hr - %p You may have mistyped the address or the page may have moved. + %h3 + = s_('404|Page Not Found') + %p + = s_("404|Make sure the address is correct and the page hasn't moved.") + %p + = s_('404|Please contact your GitLab administrator if you think this is a mistake.') + .action-container + = form_tag search_path, method: :get, class: 'form-inline-flex' do |f| + .field + = search_field_tag :search, '', placeholder: _('Search for projects, issues, etc.'), class: 'form-control' + = button_tag 'Search', class: 'btn btn-success', name: nil, type: 'submit' += render 'errors/footer' diff --git a/app/views/groups/_create_chat_team.html.haml b/app/views/groups/_create_chat_team.html.haml index 9a3ff0313b5..f950968030f 100644 --- a/app/views/groups/_create_chat_team.html.haml +++ b/app/views/groups/_create_chat_team.html.haml @@ -5,8 +5,8 @@ Mattermost .col-sm-10 .form-check.js-toggle-container - = f.label :create_chat_team do - .js-toggle-button= f.check_box(:create_chat_team, { checked: true }, true, false) + .js-toggle-button.form-check-input= f.check_box(:create_chat_team, { checked: true }, true, false) + = f.label :create_chat_team, class: 'form-check-label' do Create a Mattermost team for this group %br %small.light.js-toggle-content diff --git a/app/views/groups/_group_admin_settings.html.haml b/app/views/groups/_group_admin_settings.html.haml index 3cd3fb32b9c..f7cc62c6929 100644 --- a/app/views/groups/_group_admin_settings.html.haml +++ b/app/views/groups/_group_admin_settings.html.haml @@ -2,8 +2,8 @@ = f.label :lfs_enabled, 'Large File Storage', class: 'col-form-label col-sm-2' .col-sm-10 .form-check - = f.label :lfs_enabled do - = f.check_box :lfs_enabled, checked: @group.lfs_enabled? + = f.check_box :lfs_enabled, checked: @group.lfs_enabled?, class: 'form-check-input' + = f.label :lfs_enabled, class: 'form-check-label' do %strong Allow projects within this group to use Git LFS = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') @@ -14,8 +14,8 @@ = f.label :require_two_factor_authentication, 'Two-factor authentication', class: 'col-form-label col-sm-2' .col-sm-10 .form-check - = f.label :require_two_factor_authentication do - = f.check_box :require_two_factor_authentication + = f.check_box :require_two_factor_authentication, class: 'form-check-input' + = f.label :require_two_factor_authentication, class: 'form-check-label' do %strong Require all users in this group to setup Two-factor authentication = link_to icon('question-circle'), help_page_path('security/two_factor_authentication', anchor: 'enforcing-2fa-for-all-users-in-a-group') diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index 662db18cf86..8037cf4b69d 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -8,10 +8,7 @@ .top-area = render 'shared/issuable/nav', type: :issues .nav-controls - = link_to safe_params.merge(rss_url_options), class: 'btn' do - = icon('rss') - %span.icon-label - Subscribe + = render 'shared/issuable/feed_buttons' = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", type: :issues = render 'shared/issuable/search_bar', type: :issues diff --git a/app/views/groups/issues_calendar.ics.haml b/app/views/groups/issues_calendar.ics.haml new file mode 100644 index 00000000000..59573e5fecf --- /dev/null +++ b/app/views/groups/issues_calendar.ics.haml @@ -0,0 +1 @@ += render 'issues/issues_calendar', issues: @issues diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml index 15a5ecf791c..f1f67af1d1e 100644 --- a/app/views/groups/settings/_permissions.html.haml +++ b/app/views/groups/settings/_permissions.html.haml @@ -13,8 +13,8 @@ = s_('GroupSettings|Share with group lock') .col-sm-10 .form-check - = f.label :share_with_group_lock do - = f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group) + = f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group), class: 'form-check-input' + = f.label :share_with_group_lock, class: 'form-check-label' do %strong - group_link = link_to @group.name, group_path(@group) = s_('GroupSettings|Prevent sharing a project within %{group} with other groups').html_safe % { group: group_link } diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index a06db85ef6f..de8369ed7b9 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -472,8 +472,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - %label - %input{ :type => "checkbox" }/ + %input.form-check-input{ :type => "checkbox" }/ + %label.form-check-label Remember me .form-group.row .offset-sm-2.col-sm-10 @@ -492,8 +492,8 @@ %label{ :for => "exampleInputPassword1" } Password %input#exampleInputPassword1.form-control{ :placeholder => "Password", :type => "password" }/ .form-check - %label - %input{ :type => "checkbox" }/ + %input.form-check-input{ :type => "checkbox" }/ + %label.form-check-label Remember me %button.btn.btn-default{ :type => "submit" } Sign in diff --git a/app/views/import/fogbugz/new.html.haml b/app/views/import/fogbugz/new.html.haml index d5a37095365..74d686b6703 100644 --- a/app/views/import/fogbugz/new.html.haml +++ b/app/views/import/fogbugz/new.html.haml @@ -11,16 +11,16 @@ In the next steps, you'll be able to map users and select the projects you want to import. .form-group.row - = label_tag :uri, 'FogBugz URL', class: 'col-form-label col-sm-8' - .col-sm-4 + = label_tag :uri, 'FogBugz URL', class: 'col-form-label col-md-2' + .col-md-4 = text_field_tag :uri, nil, placeholder: 'https://mycompany.fogbugz.com', class: 'form-control' .form-group.row - = label_tag :email, 'FogBugz Email', class: 'col-form-label col-sm-8' - .col-sm-4 + = label_tag :email, 'FogBugz Email', class: 'col-form-label col-md-2' + .col-md-4 = text_field_tag :email, nil, class: 'form-control' .form-group.row - = label_tag :password, 'FogBugz Password', class: 'col-form-label col-sm-8' - .col-sm-4 + = label_tag :password, 'FogBugz Password', class: 'col-form-label col-md-2' + .col-md-4 = password_field_tag :password, nil, class: 'form-control' .form-actions = submit_tag 'Continue to the next step', class: 'btn btn-create' diff --git a/app/views/issues/_issues_calendar.ics.ruby b/app/views/issues/_issues_calendar.ics.ruby new file mode 100644 index 00000000000..3563635d33d --- /dev/null +++ b/app/views/issues/_issues_calendar.ics.ruby @@ -0,0 +1,15 @@ +cal = Icalendar::Calendar.new +cal.prodid = '-//GitLab//NONSGML GitLab//EN' +cal.x_wr_calname = 'GitLab Issues' + +@issues.includes(project: :namespace).each do |issue| + cal.event do |event| + event.dtstart = Icalendar::Values::Date.new(issue.due_date) + event.summary = "#{issue.title} (in #{issue.project.full_path})" + event.description = "Find out more at #{issue_url(issue)}" + event.url = issue_url(issue) + event.transp = 'TRANSPARENT' + end +end + +cal.to_ical diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml index 9382ee8715e..06069a72951 100644 --- a/app/views/layouts/errors.html.haml +++ b/app/views/layouts/errors.html.haml @@ -3,57 +3,17 @@ %head %meta{ :content => "width=device-width, initial-scale=1, maximum-scale=1", :name => "viewport" } %title= yield(:title) - :css - body { - color: #666; - text-align: center; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - margin: auto; - font-size: 14px; - } + %style + = Rails.application.assets_manifest.find_sources('errors.css').first.to_s.html_safe + %body + .page-container + = yield + -# haml-lint:disable InlineJavaScript + :javascript + (function(){ + var goBackElement = document.querySelector('.js-go-back'); - h1 { - font-size: 56px; - line-height: 100px; - font-weight: 400; - color: #456; - } - - h2 { - font-size: 24px; - color: #666; - line-height: 1.5em; - } - - h3 { - color: #456; - font-size: 20px; - font-weight: 400; - line-height: 28px; - } - - hr { - max-width: 800px; - margin: 18px auto; - border: 0; - border-top: 1px solid #EEE; - border-bottom: 1px solid white; - } - - img { - max-width: 40vw; - display: block; - margin: 40px auto; - } - - .container { - margin: auto 20px; - } - - ul { - margin: auto; - text-align: left; - display:inline-block; - } -%body - = yield + if (goBackElement && history.length > 1) { + goBackElement.style.display = 'block'; + } + }()); diff --git a/app/views/peek/_bar.html.haml b/app/views/peek/_bar.html.haml index cb0cccb8f8a..89d3b931f88 100644 --- a/app/views/peek/_bar.html.haml +++ b/app/views/peek/_bar.html.haml @@ -2,6 +2,6 @@ #js-peek{ data: { env: Peek.env, request_id: Peek.request_id, - peek_url: peek_routes.results_url, + peek_url: "#{peek_routes_path}/results", profile_url: url_for(safe_params.merge(lineprofiler: 'true')) }, class: Peek.env } diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index d253e8e456e..d111113c646 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -34,18 +34,18 @@ .row.prepend-top-default .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 - RSS token + Feed token %p - Your RSS token is used to authenticate you when your RSS reader loads a personalized RSS feed, and is included in your personal RSS feed URLs. + Your feed token is used to authenticate you when your RSS reader loads a personalized RSS feed or when when your calendar application loads a personalized calendar, and is included in those feed URLs. %p It cannot be used to access any other data. - .col-lg-8.rss-token-reset - = label_tag :rss_token, 'RSS token', class: "label-light" - = text_field_tag :rss_token, current_user.rss_token, class: 'form-control', readonly: true, onclick: 'this.select()' + .col-lg-8.feed-token-reset + = label_tag :feed_token, 'Feed token', class: "label-light" + = text_field_tag :feed_token, current_user.feed_token, class: 'form-control', readonly: true, onclick: 'this.select()' %p.form-text.text-muted - Keep this token secret. Anyone who gets ahold of it can read activity and issue RSS feeds as if they were you. + Keep this token secret. Anyone who gets ahold of it can read activity and issue RSS feeds or your calendar feed as if they were you. You should - = link_to 'reset it', [:reset, :rss_token, :profile], method: :put, data: { confirm: 'Are you sure? Any RSS URLs currently in use will stop working.' } + = link_to 'reset it', [:reset, :feed_token, :profile], method: :put, data: { confirm: 'Are you sure? Any RSS or calendar URLs currently in use will stop working.' } if that ever happens. - if incoming_email_token_enabled? diff --git a/app/views/projects/_import_project_pane.html.haml b/app/views/projects/_import_project_pane.html.haml index 4bee6cb97eb..8f535b9d789 100644 --- a/app/views/projects/_import_project_pane.html.haml +++ b/app/views/projects/_import_project_pane.html.haml @@ -1,51 +1,49 @@ - active_tab = local_assigns.fetch(:active_tab, 'blank') - f = local_assigns.fetch(:f) -.project-import.row - .col-lg-12 - .form-group.import-btn-container.clearfix - = f.label :visibility_level, class: 'label-light' do #the label here seems wrong - Import project from - .import-buttons - - if gitlab_project_import_enabled? - .import_gitlab_project.has-tooltip{ data: { container: 'body' } } - = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do - = icon('gitlab', text: 'GitLab export') - %div - - if github_import_enabled? - = link_to new_import_github_path, class: 'btn js-import-github' do - = icon('github', text: 'GitHub') - %div - - if bitbucket_import_enabled? - = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do - = icon('bitbucket', text: 'Bitbucket') - - unless bitbucket_import_configured? - = render 'bitbucket_import_modal' - %div - - if gitlab_import_enabled? - = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do - = icon('gitlab', text: 'GitLab.com') - - unless gitlab_import_configured? - = render 'gitlab_import_modal' - %div - - if google_code_import_enabled? - = link_to new_import_google_code_path, class: 'btn import_google_code' do - = icon('google', text: 'Google Code') - %div - - if fogbugz_import_enabled? - = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do - = icon('bug', text: 'Fogbugz') - %div - - if gitea_import_enabled? - = link_to new_import_gitea_path, class: 'btn import_gitea' do - = custom_icon('go_logo') - Gitea - %div - - if git_import_enabled? - %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } } - = icon('git', text: 'Repo by URL') - .col-lg-12 - .js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') } - %hr - = render "shared/import_form", f: f - = render 'new_project_fields', f: f, project_name_id: "import-url-name" +.project-import + .form-group.import-btn-container.clearfix + = f.label :visibility_level, class: 'label-light' do #the label here seems wrong + Import project from + .import-buttons + - if gitlab_project_import_enabled? + .import_gitlab_project.has-tooltip{ data: { container: 'body' } } + = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do + = icon('gitlab', text: 'GitLab export') + %div + - if github_import_enabled? + = link_to new_import_github_path, class: 'btn js-import-github' do + = icon('github', text: 'GitHub') + %div + - if bitbucket_import_enabled? + = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do + = icon('bitbucket', text: 'Bitbucket') + - unless bitbucket_import_configured? + = render 'bitbucket_import_modal' + %div + - if gitlab_import_enabled? + = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do + = icon('gitlab', text: 'GitLab.com') + - unless gitlab_import_configured? + = render 'gitlab_import_modal' + %div + - if google_code_import_enabled? + = link_to new_import_google_code_path, class: 'btn import_google_code' do + = icon('google', text: 'Google Code') + %div + - if fogbugz_import_enabled? + = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do + = icon('bug', text: 'Fogbugz') + %div + - if gitea_import_enabled? + = link_to new_import_gitea_path, class: 'btn import_gitea' do + = custom_icon('go_logo') + Gitea + %div + - if git_import_enabled? + %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } } + = icon('git', text: 'Repo by URL') + .js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') } + %hr + = render "shared/import_form", f: f + = render 'new_project_fields', f: f, project_name_id: "import-url-name" diff --git a/app/views/projects/_merge_request_fast_forward_settings.html.haml b/app/views/projects/_merge_request_fast_forward_settings.html.haml deleted file mode 100644 index 2f08a28e26e..00000000000 --- a/app/views/projects/_merge_request_fast_forward_settings.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -- form = local_assigns.fetch(:form) -- project = local_assigns.fetch(:project) - -.form-check - = label_tag :project_merge_method_ff do - = form.radio_button :merge_method, :ff, class: "js-merge-method-radio qa-radio-button-merge-ff" - %strong Fast-forward merge - %br - %span.descr - No merge commits are created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded. - %br - %span.descr - When fast-forward merge is not possible, the user is given the option to rebase. diff --git a/app/views/projects/_merge_request_merge_method_settings.html.haml b/app/views/projects/_merge_request_merge_method_settings.html.haml new file mode 100644 index 00000000000..3bb220ac6d0 --- /dev/null +++ b/app/views/projects/_merge_request_merge_method_settings.html.haml @@ -0,0 +1,36 @@ +- form = local_assigns.fetch(:form) +- project = local_assigns.fetch(:project) + +.form-group + = label_tag :merge_method_merge, class: 'label-light' do + Merge method + .form-check + = form.radio_button :merge_method, :merge, class: "js-merge-method-radio form-check-input" + = label_tag :project_merge_method_merge, class: 'form-check-label' do + %strong Merge commit + %br + %span.descr + A merge commit is created for every merge, and merging is allowed as long as there are no conflicts. + + .form-check + = form.radio_button :merge_method, :rebase_merge, class: "js-merge-method-radio form-check-input" + = label_tag :project_merge_method_rebase_merge, class: 'form-check-label' do + %strong Merge commit with semi-linear history + %br + %span.descr + A merge commit is created for every merge, but merging is only allowed if fast-forward merge is possible. + This way you could make sure that if this merge request would build, after merging to target branch it would also build. + %br + %span.descr + When fast-forward merge is not possible, the user is given the option to rebase. + + .form-check + = form.radio_button :merge_method, :ff, class: "js-merge-method-radio qa-radio-button-merge-ff form-check-input" + = label_tag :project_merge_method_ff, class: 'form-check-label' do + %strong Fast-forward merge + %br + %span.descr + No merge commits are created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded. + %br + %span.descr + When fast-forward merge is not possible, the user is given the option to rebase. diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml index 762a263656d..f178c94e008 100644 --- a/app/views/projects/_merge_request_merge_settings.html.haml +++ b/app/views/projects/_merge_request_merge_settings.html.haml @@ -2,22 +2,22 @@ .form-group .form-check.builds-feature{ class: ("hidden" if @project && @project.project_feature.send(:builds_access_level) == 0) } - = form.label :only_allow_merge_if_pipeline_succeeds do - = form.check_box :only_allow_merge_if_pipeline_succeeds + = form.check_box :only_allow_merge_if_pipeline_succeeds, class: 'form-check-input' + = form.label :only_allow_merge_if_pipeline_succeeds, class: 'form-check-label' do %strong Only allow merge requests to be merged if the pipeline succeeds %br %span.descr Pipelines need to be configured to enable this feature. = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds'), target: '_blank' .form-check - = form.label :only_allow_merge_if_all_discussions_are_resolved do - = form.check_box :only_allow_merge_if_all_discussions_are_resolved + = form.check_box :only_allow_merge_if_all_discussions_are_resolved, class: 'form-check-input' + = form.label :only_allow_merge_if_all_discussions_are_resolved, class: 'form-check-label' do %strong Only allow merge requests to be merged if all discussions are resolved .form-check - = form.label :resolve_outdated_diff_discussions do - = form.check_box :resolve_outdated_diff_discussions + = form.check_box :resolve_outdated_diff_discussions, class: 'form-check-input' + = form.label :resolve_outdated_diff_discussions, class: 'form-check-label' do %strong Automatically resolve merge request diff discussions when they become outdated .form-check - = form.label :printing_merge_request_link_enabled do - = form.check_box :printing_merge_request_link_enabled + = form.check_box :printing_merge_request_link_enabled, class: 'form-check-input' + = form.label :printing_merge_request_link_enabled, class: 'form-check-label' do %strong Show link to create/view merge request when pushing from the command line diff --git a/app/views/projects/_merge_request_rebase_settings.html.haml b/app/views/projects/_merge_request_rebase_settings.html.haml deleted file mode 100644 index 93895a55435..00000000000 --- a/app/views/projects/_merge_request_rebase_settings.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -- form = local_assigns.fetch(:form) - -.form-check - = label_tag :project_merge_method_rebase_merge do - = form.radio_button :merge_method, :rebase_merge, class: "js-merge-method-radio" - %strong Merge commit with semi-linear history - %br - %span.descr - A merge commit is created for every merge, but merging is only allowed if fast-forward merge is possible. - This way you could make sure that if this merge request would build, after merging to target branch it would also build. - %br - %span.descr - When fast-forward merge is not possible, the user is given the option to rebase. diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml index a9ddcd94865..c80e831dd33 100644 --- a/app/views/projects/_merge_request_settings.html.haml +++ b/app/views/projects/_merge_request_settings.html.haml @@ -1,18 +1,5 @@ - form = local_assigns.fetch(:form) -.form-group - = label_tag :merge_method_merge, class: 'label-light' do - Merge method - .form-check - = label_tag :project_merge_method_merge do - = form.radio_button :merge_method, :merge, class: "js-merge-method-radio" - %strong Merge commit - %br - %span.descr - A merge commit is created for every merge, and merging is allowed as long as there are no conflicts. - - = render 'merge_request_rebase_settings', form: form - - = render 'merge_request_fast_forward_settings', project: @project, form: form += render 'projects/merge_request_merge_method_settings', project: @project, form: form = render 'projects/merge_request_merge_settings', form: form diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml index 6366a2f729a..cfbd0459e3e 100644 --- a/app/views/projects/_new_project_fields.html.haml +++ b/app/views/projects/_new_project_fields.html.haml @@ -35,11 +35,10 @@ %span (optional) = f.text_area :description, placeholder: 'Description format', class: "form-control", rows: 3, maxlength: 250 -.form-group.visibility-level-setting - = f.label :visibility_level, class: 'label-light' do - Visibility Level - = link_to icon('question-circle'), help_page_path("public_access/public_access"), aria: { label: 'Documentation for Visibility Level' }, target: '_blank', rel: 'noopener noreferrer' - = render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false += f.label :visibility_level, class: 'label-light' do + Visibility Level + = link_to icon('question-circle'), help_page_path("public_access/public_access"), aria: { label: 'Documentation for Visibility Level' }, target: '_blank', rel: 'noopener noreferrer' += render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false = f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4 = link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel' diff --git a/app/views/projects/clusters/gcp/login.html.haml b/app/views/projects/clusters/gcp/login.html.haml index f1771349a53..55a42ac4847 100644 --- a/app/views/projects/clusters/gcp/login.html.haml +++ b/app/views/projects/clusters/gcp/login.html.haml @@ -12,8 +12,7 @@ .row .col-sm-8.offset-sm-4.signin-with-google - if @authorize_url - = link_to @authorize_url do - = image_tag('auth_buttons/signin_with_google.png', width: '191px') + = link_to(image_tag('auth_buttons/signin_with_google.png', width: '191px'), @authorize_url) = _('or') = link_to('create a new Google account', 'https://accounts.google.com/SignUpWithoutGmail?service=cloudconsole&continue=https%3A%2F%2Fconsole.cloud.google.com%2Ffreetrial%3Futm_campaign%3D2018_cpanel%26utm_source%3Dgitlab%26utm_medium%3Dreferral', target: '_blank', rel: 'noopener noreferrer') - else diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 79b22766bc7..77ba422e87e 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -33,10 +33,15 @@ %span.light (optional) = f.text_area :description, class: "form-control", rows: 3, maxlength: 250 + = render_if_exists 'projects/classification_policy_settings', f: f + - unless @project.empty_repo? .form-group = f.label :default_branch, "Default Branch", class: 'label-light' = f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'}) + + = render_if_exists 'shared/repository_size_limit_setting', form: f, type: :project + .form-group = f.label :tag_list, "Tags", class: 'label-light' = f.text_field :tag_list, value: @project.tag_list.sort.join(', '), maxlength: 2000, class: "form-control" @@ -75,6 +80,8 @@ .js-project-permissions-form = f.submit 'Save changes', class: "btn btn-save" + = render_if_exists 'projects/issues_settings' + %section.settings.merge-requests-feature.no-animate{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)] } .settings-header %h4 @@ -84,10 +91,14 @@ %p Customize your merge request restrictions. .settings-content + = render_if_exists 'shared/promotions/promote_mr_features' + = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings-form" }, authenticity_token: true do |f| - = render 'merge_request_settings', form: f + = render 'projects/merge_request_settings', form: f = f.submit 'Save changes', class: "btn btn-save qa-save-merge-request-changes" + = render_if_exists 'projects/service_desk_settings' + = render 'export', project: @project %section.settings.advanced-settings.no-animate{ class: ('expanded' if expanded) } diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml index 297b928f020..0dd2d2e6c5d 100644 --- a/app/views/projects/issues/_nav_btns.html.haml +++ b/app/views/projects/issues/_nav_btns.html.haml @@ -1,5 +1,5 @@ -= link_to safe_params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do - = icon('rss') += render 'shared/issuable/feed_buttons' + - if @can_bulk_update = button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle" - if show_new_issue_link?(@project) diff --git a/app/views/projects/issues/calendar.ics.haml b/app/views/projects/issues/calendar.ics.haml new file mode 100644 index 00000000000..59573e5fecf --- /dev/null +++ b/app/views/projects/issues/calendar.ics.haml @@ -0,0 +1 @@ += render 'issues/issues_calendar', issues: @issues diff --git a/app/views/projects/mattermosts/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml index 361d3c61d99..37c09f12f63 100644 --- a/app/views/projects/mattermosts/_team_selection.html.haml +++ b/app/views/projects/mattermosts/_team_selection.html.haml @@ -13,9 +13,9 @@ = f.hidden_field(:team_id, value: selected_id, required: true) if @teams.one? .form-text.text-muted - if @teams.one? - This is the only available team. + This is the only available team that you are a member of. - else - The list shows all available teams. + The list shows all available teams that you are a member of. To create a team, = link_to "#{Gitlab.config.mattermost.host}/create_team" do use Mattermost's interface diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index aa3fb623e58..01e38ffee20 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -20,6 +20,8 @@ window.gl = window.gl || {}; window.gl.mrWidgetData = #{serialize_issuable(@merge_request, serializer: 'widget')} + window.gl.mrWidgetData.squash_before_merge_help_path = '#{help_page_path("user/project/merge_requests/squash_and_merge")}'; + #js-vue-mr-widget.mr-widget .content-block.content-block-small.emoji-list-container.js-noteable-awards diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index 75cb8245d36..2d3f9116703 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -10,8 +10,8 @@ = icon('search') .inline.prepend-left-20 .form-check.light - = label_tag :filter_ref do - = check_box_tag :filter_ref, 1, @options[:filter_ref] + = check_box_tag :filter_ref, 1, @options[:filter_ref], class: 'form-check-input' + = label_tag :filter_ref, class: 'form-check-label' do %span= _("Begin with the selected commit") - if @commit diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml index 414df15feeb..bbabb98dafe 100644 --- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml +++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml @@ -11,22 +11,22 @@ = message.html_safe = f.fields_for :auto_devops_attributes, @auto_devops do |form| .form-check - = form.label :enabled_true do - = form.radio_button :enabled, 'true' + = form.radio_button :enabled, 'true', class: 'form-check-input' + = form.label :enabled_true, class: 'form-check-label' do %strong= s_('CICD|Enable Auto DevOps') %br = s_('CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project.').html_safe % { ci_file: ci_file_formatted } .form-check - = form.label :enabled_false do - = form.radio_button :enabled, 'false' + = form.radio_button :enabled, 'false', class: 'form-check-input' + = form.label :enabled_false, class: 'form-check-label' do %strong= s_('CICD|Disable Auto DevOps') %br = s_('CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery.').html_safe % { ci_file: ci_file_formatted } .form-check - = form.label :enabled_ do - = form.radio_button :enabled, '' + = form.radio_button :enabled, '', class: 'form-check-input' + = form.label :enabled_, class: 'form-check-label' do %strong= s_('CICD|Instance default (%{state})') % { state: "#{Gitlab::CurrentSettings.auto_devops_enabled? ? _('enabled') : _('disabled')}" } %br = s_('CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}.').html_safe % { ci_file: ci_file_formatted } diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml index 50175f5258c..e93b240a007 100644 --- a/app/views/projects/settings/ci_cd/_form.html.haml +++ b/app/views/projects/settings/ci_cd/_form.html.haml @@ -20,15 +20,15 @@ Choose between <code>clone</code> or <code>fetch</code> to get the recent application code = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'git-strategy'), target: '_blank' .form-check - = f.label :build_allow_git_fetch_false do - = f.radio_button :build_allow_git_fetch, 'false' + = f.radio_button :build_allow_git_fetch, 'false', { class: 'form-check-input' } + = f.label :build_allow_git_fetch_false, class: 'form-check-label' do %strong git clone %br %span.descr Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job .form-check - = f.label :build_allow_git_fetch_true do - = f.radio_button :build_allow_git_fetch, 'true' + = f.radio_button :build_allow_git_fetch, 'true', { class: 'form-check-input' } + = f.label :build_allow_git_fetch_true, class: 'form-check-label' do %strong git fetch %br %span.descr @@ -53,8 +53,8 @@ %hr .form-group .form-check - = f.label :public_builds do - = f.check_box :public_builds + = f.check_box :public_builds, { class: 'form-check-input' } + = f.label :public_builds, class: 'form-check-label' do %strong Public pipelines .form-text.text-muted Allow public access to pipelines and job details, including output logs and artifacts @@ -75,8 +75,8 @@ %hr .form-group .form-check - = f.label :auto_cancel_pending_pipelines do - = f.check_box :auto_cancel_pending_pipelines, {}, 'enabled', 'disabled' + = f.check_box :auto_cancel_pending_pipelines, { class: 'form-check-input' }, 'enabled', 'disabled' + = f.label :auto_cancel_pending_pipelines, class: 'form-check-label' do %strong Auto-cancel redundant, pending pipelines .form-text.text-muted New pipelines will cancel older, pending pipelines on the same branch diff --git a/app/views/shared/_allow_request_access.html.haml b/app/views/shared/_allow_request_access.html.haml index 0e698570d7d..92268e74b1e 100644 --- a/app/views/shared/_allow_request_access.html.haml +++ b/app/views/shared/_allow_request_access.html.haml @@ -1,6 +1,6 @@ .form-check - = form.label :request_access_enabled do - = form.check_box :request_access_enabled + = form.check_box :request_access_enabled, class: 'form-check-input' + = form.label :request_access_enabled, class: 'form-check-label' do %strong Allow users to request access %br %span.descr Allow users to request access if visibility is public or internal. diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml index 35673303b85..356e12cf9f8 100644 --- a/app/views/shared/_import_form.html.haml +++ b/app/views/shared/_import_form.html.haml @@ -1,19 +1,20 @@ - ci_cd_only = local_assigns.fetch(:ci_cd_only, false) -.form-group.row.import-url-data +.form-group.import-url-data = f.label :import_url, class: 'label-light' do %span = _('Git repository URL') = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', required: true - .card.prepend-top-20 - %ul - %li - = _('The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.').html_safe - %li - = _('If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.').html_safe - %li - = import_will_timeout_message(ci_cd_only) - %li - = import_svn_message(ci_cd_only) + .info-well.prepend-top-20 + .well-segment + %ul + %li + = _('The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.').html_safe + %li + = _('If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.').html_safe + %li + = import_will_timeout_message(ci_cd_only) + %li + = import_svn_message(ci_cd_only) diff --git a/app/views/shared/_new_merge_request_checkbox.html.haml b/app/views/shared/_new_merge_request_checkbox.html.haml index 8dbdf63f9f4..165109b6b70 100644 --- a/app/views/shared/_new_merge_request_checkbox.html.haml +++ b/app/views/shared/_new_merge_request_checkbox.html.haml @@ -1,7 +1,7 @@ .form-check - nonce = SecureRandom.hex - = label_tag "create_merge_request-#{nonce}" do - = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}" + = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request form-check-input', id: "create_merge_request-#{nonce}" + = label_tag "create_merge_request-#{nonce}", class: 'form-check-label' do - translation_variables = { new_merge_request: "<strong>#{_('new merge request')}</strong>" } - translation = _('Start a %{new_merge_request} with these changes') % translation_variables #{ translation.html_safe } diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml index 38c6f560dc6..d67409ffe14 100644 --- a/app/views/shared/_visibility_level.html.haml +++ b/app/views/shared/_visibility_level.html.haml @@ -1,6 +1,6 @@ - with_label = local_assigns.fetch(:with_label, true) -.form-group.row.visibility-level-setting +.form-group.visibility-level-setting - if with_label = f.label :visibility_level, class: 'col-form-label col-sm-2' do Visibility Level diff --git a/app/views/shared/_visibility_radios.html.haml b/app/views/shared/_visibility_radios.html.haml index bdf550cd8b6..dd6b9cce58e 100644 --- a/app/views/shared/_visibility_radios.html.haml +++ b/app/views/shared/_visibility_radios.html.haml @@ -2,9 +2,9 @@ - disallowed = disallowed_visibility_level?(form_model, level) - restricted = restricted_visibility_levels.include?(level) - disabled = disallowed || restricted - .form-check.pl-0{ class: [('disabled' if disabled), ('restricted' if restricted)] } - = form.label "#{model_method}_#{level}" do - = form.radio_button model_method, level, checked: (selected_level == level), disabled: disabled + .form-check{ class: [('disabled' if disabled), ('restricted' if restricted)] } + = form.radio_button model_method, level, checked: (selected_level == level), disabled: disabled, class: 'form-check-input' + = form.label "#{model_method}_#{level}", class: 'form-check-label' do = visibility_level_icon(level) .option-title = visibility_level_label(level) diff --git a/app/views/shared/dashboard/_no_filter_selected.html.haml b/app/views/shared/dashboard/_no_filter_selected.html.haml index b2e6967f6aa..32246dac4c7 100644 --- a/app/views/shared/dashboard/_no_filter_selected.html.haml +++ b/app/views/shared/dashboard/_no_filter_selected.html.haml @@ -1,8 +1,8 @@ .row.empty-state.text-center - .col-xs-12 + .col-12 .svg-130.prepend-top-default = image_tag 'illustrations/issue-dashboard_results-without-filter.svg' - .col-xs-12 + .col-12 .text-content %h4 = _("Please select at least one filter to see results") diff --git a/app/views/shared/empty_states/_wikis_layout.html.haml b/app/views/shared/empty_states/_wikis_layout.html.haml index 6fae6104ca2..a5f100e3469 100644 --- a/app/views/shared/empty_states/_wikis_layout.html.haml +++ b/app/views/shared/empty_states/_wikis_layout.html.haml @@ -1,7 +1,7 @@ .row.empty-state - .col-xs-12 + .col-12 .svg-content = image_tag image_path - .col-xs-12 + .col-12 .text-content.text-center = yield diff --git a/app/views/shared/icons/_icon_calendar.svg b/app/views/shared/icons/_icon_calendar.svg new file mode 100644 index 00000000000..4d0a703f9a0 --- /dev/null +++ b/app/views/shared/icons/_icon_calendar.svg @@ -0,0 +1 @@ +<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M15 5v7a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V5a2 2 0 0 1 2-2h1V2a1 1 0 1 1 2 0v1h4V2a1 1 0 1 1 2 0v1h1a2 2 0 0 1 2 2zM3 6v6a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V6H3zm2 2h2a1 1 0 1 1 0 2H5a1 1 0 1 1 0-2z" fill="#000" fill-rule="evenodd"/></svg>
\ No newline at end of file diff --git a/app/views/shared/issuable/_feed_buttons.html.haml b/app/views/shared/issuable/_feed_buttons.html.haml new file mode 100644 index 00000000000..d4834090413 --- /dev/null +++ b/app/views/shared/issuable/_feed_buttons.html.haml @@ -0,0 +1,4 @@ += link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe to RSS feed' do + = icon('rss') += link_to safe_params.merge(calendar_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe to calendar' do + = custom_icon('icon_calendar') diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index fbc608b207a..8497e3220ee 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -23,8 +23,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = form.label :confidential do - = form.check_box :confidential + = form.check_box :confidential, class: 'form-check-input' + = form.label :confidential, class: 'form-check-label' do This issue is confidential and should only be visible to team members with at least Reporter access. = render 'shared/issuable/form/metadata', issuable: issuable, form: form diff --git a/app/views/shared/issuable/form/_contribution.html.haml b/app/views/shared/issuable/form/_contribution.html.haml index f12c23cbc64..b34549240e0 100644 --- a/app/views/shared/issuable/form/_contribution.html.haml +++ b/app/views/shared/issuable/form/_contribution.html.haml @@ -12,8 +12,8 @@ = _('Contribution') .col-sm-10 .form-check - = form.label :allow_maintainer_to_push do - = form.check_box :allow_maintainer_to_push, disabled: !issuable.can_allow_maintainer_to_push?(current_user) + = form.check_box :allow_maintainer_to_push, disabled: !issuable.can_allow_maintainer_to_push?(current_user), class: 'form-check-input' + = form.label :allow_maintainer_to_push, class: 'form-check-label' do = _('Allow edits from maintainers.') = link_to 'About this feature', help_page_path('user/project/merge_requests/maintainer_access') .form-text.text-muted diff --git a/app/views/shared/issuable/form/_default_templates.html.haml b/app/views/shared/issuable/form/_default_templates.html.haml new file mode 100644 index 00000000000..49a5ce926b3 --- /dev/null +++ b/app/views/shared/issuable/form/_default_templates.html.haml @@ -0,0 +1,4 @@ +%p.form-text.text-muted + Add + = link_to 'description templates', help_page_path('user/project/description_templates'), tabindex: -1 + to help your contributors communicate effectively! diff --git a/app/views/shared/issuable/form/_merge_params.html.haml b/app/views/shared/issuable/form/_merge_params.html.haml index 1df881e4102..1bcdddd0df0 100644 --- a/app/views/shared/issuable/form/_merge_params.html.haml +++ b/app/views/shared/issuable/form/_merge_params.html.haml @@ -11,7 +11,16 @@ .col-sm-10.offset-sm-2 - if issuable.can_remove_source_branch?(current_user) .form-check - = label_tag 'merge_request[force_remove_source_branch]' do - = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil - = check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch? + = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil + = check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch?, class: 'form-check-input' + = label_tag 'merge_request[force_remove_source_branch]', class: 'form-check-label' do Remove source branch when merge request is accepted. + +.form-group.row + .col-sm-10.offset-sm-2 + .form-check + = hidden_field_tag 'merge_request[squash]', '0', id: nil + = check_box_tag 'merge_request[squash]', '1', issuable.squash, class: 'form-check-input' + = label_tag 'merge_request[squash]', class: 'form-check-label' do + Squash commits when merge request is accepted. + = link_to 'About this feature', help_page_path('user/project/merge_requests/squash_and_merge') diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml index c4f30f5f4d9..c35d0b3751f 100644 --- a/app/views/shared/issuable/form/_title.html.haml +++ b/app/views/shared/issuable/form/_title.html.haml @@ -30,7 +30,4 @@ merge request from being merged before it's ready. - if no_issuable_templates && can?(current_user, :push_code, issuable.project) - %p.form-text.text-muted - Add - = link_to 'description templates', help_page_path('user/project/description_templates'), tabindex: -1 - to help your contributors communicate effectively! + = render 'shared/issuable/form/default_templates' diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 42b2d27c44a..46debe1f2b9 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -61,7 +61,7 @@ title: 'Resend invite' - if user != current_user && member.can_update? - = form_for member, remote: true, html: { class: 'js-edit-member-form form-horizontal' } do |f| + = form_for member, remote: true, html: { class: 'js-edit-member-form form-group row append-right-5' } do |f| = f.hidden_field :access_level .member-form-control.dropdown.append-right-5 %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button", diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml index e6a65161ed6..55460acab8f 100644 --- a/app/views/shared/milestones/_tabs.html.haml +++ b/app/views/shared/milestones/_tabs.html.haml @@ -5,25 +5,25 @@ .fade-right= icon('angle-right') %ul.nav-links.scrolling-tabs.js-milestone-tabs.nav.nav-tabs - if issues_accessible - %li.active - = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do + %li.nav-item + = link_to '#tab-issues', class: 'nav-link active', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do Issues %span.badge.badge-pill= milestone.issues_visible_to_user(current_user).size - %li - = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do + %li.nav-item + = link_to '#tab-merge-requests', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do Merge Requests %span.badge.badge-pill= milestone.merge_requests.size - else - %li.active - = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do + %li.nav-item + = link_to '#tab-merge-requests', class: 'nav-link active', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do Merge Requests %span.badge.badge-pill= milestone.merge_requests.size - %li - = link_to '#tab-participants', 'data-toggle' => 'tab', 'data-endpoint': milestone_participants_tab_path(milestone) do + %li.nav-item + = link_to '#tab-participants', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_participants_tab_path(milestone) do Participants %span.badge.badge-pill= milestone.participants.count - %li - = link_to '#tab-labels', 'data-toggle' => 'tab', 'data-endpoint': milestone_labels_tab_path(milestone) do + %li.nav-item + = link_to '#tab-labels', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_labels_tab_path(milestone) do Labels %span.badge.badge-pill= milestone.labels.count diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml index d830225d169..51d912b4a66 100644 --- a/app/views/shared/notifications/_custom_notifications.html.haml +++ b/app/views/shared/notifications/_custom_notifications.html.haml @@ -23,8 +23,8 @@ - field_id = "#{notifications_menu_identifier("modal", notification_setting)}_notification_setting[#{event}]" .form-group .form-check{ class: ("prepend-top-0" if index == 0) } - %label{ for: field_id } - = check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.public_send(event)) + = check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event form-check-input", checked: notification_setting.public_send(event)) + %label.form-check-label{ for: field_id } %strong = notification_event_name(event) = icon("spinner spin", class: "custom-notification-event-loading") diff --git a/app/views/shared/runners/_form.html.haml b/app/views/shared/runners/_form.html.haml index 660123d8b07..0337680d79b 100644 --- a/app/views/shared/runners/_form.html.haml +++ b/app/views/shared/runners/_form.html.haml @@ -4,26 +4,26 @@ = label :active, "Active", class: 'col-form-label col-sm-2' .col-sm-10 .form-check - = f.check_box :active + = f.check_box :active, { class: 'form-check-input' } %span.light Paused Runners don't accept new jobs .form-group.row = label :protected, "Protected", class: 'col-form-label col-sm-2' .col-sm-10 .form-check - = f.check_box :access_level, {}, 'ref_protected', 'not_protected' + = f.check_box :access_level, { class: 'form-check-input' }, 'ref_protected', 'not_protected' %span.light This runner will only run on pipelines triggered on protected branches .form-group.row = label :run_untagged, 'Run untagged jobs', class: 'col-form-label col-sm-2' .col-sm-10 .form-check - = f.check_box :run_untagged + = f.check_box :run_untagged, { class: 'form-check-input' } %span.light Indicates whether this runner can pick jobs without tags - unless runner.group_type? .form-group.row = label :locked, _('Lock to current projects'), class: 'col-form-label col-sm-2' .col-sm-10 .form-check - = f.check_box :locked + = f.check_box :locked, { class: 'form-check-input' } %span.light= _('When a runner is locked, it cannot be assigned to other projects') .form-group.row = label_tag :token, class: 'col-form-label col-sm-2' do diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index ddf54866b99..828ec870dc0 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -32,10 +32,10 @@ - if public_snippet? .embed-snippet .input-group - .input-group-btn - %button.btn.embed-toggle{ 'data-toggle': 'dropdown', type: 'button' } + .input-group-prepend + %button.btn.btn-svg.embed-toggle.input-group-text{ 'data-toggle': 'dropdown', type: 'button' } %span.js-embed-action= _("Embed") - = sprite_icon('angle-down', size: 12) + = sprite_icon('angle-down', size: 12, css_class: 'caret-down') %ul.dropdown-menu.dropdown-menu-selectable.embed-toggle-list %li %button.js-embed-btn.btn.btn-transparent.is-active{ type: 'button' } @@ -44,7 +44,6 @@ %button.js-share-btn.btn.btn-transparent{ type: 'button' } %strong.embed-toggle-list-item= _("Share") %input.js-snippet-url-area.snippet-embed-input.form-control{ type: "text", autocomplete: 'off', value: snippet_embed } - .input-group-btn - %button.js-clipboard-btn.snippet-clipboard-btn.btn.btn-default.has-tooltip{ title: "Copy to clipboard", 'data-clipboard-target': '.js-snippet-url-area' } - = sprite_icon('duplicate', size: 16) + .input-group-append + = clipboard_button(title: s_('Copy to clipboard'), class: 'js-clipboard-btn snippet-clipboard-btn btn btn-default', target: '.js-snippet-url-area') .clearfix diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index 0d1c007dd78..660769fa50d 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -10,80 +10,70 @@ Use this token to validate received payloads. It will be sent with the request in the X-Gitlab-Token HTTP header. .form-group = form.label :url, 'Trigger', class: 'label-light' - %ul.list-unstyled + %ul.list-unstyled.prepend-left-20 %li - = form.check_box :push_events, class: 'float-left' - .prepend-left-20 - = form.label :push_events, class: 'list-label' do - %strong Push events - %p.light - This URL will be triggered by a push to the repository + = form.check_box :push_events, class: 'form-check-input' + = form.label :push_events, class: 'list-label form-check-label ml-1' do + %strong Push events + %p.light.ml-1 + This URL will be triggered by a push to the repository %li - = form.check_box :tag_push_events, class: 'float-left' - .prepend-left-20 - = form.label :tag_push_events, class: 'list-label' do - %strong Tag push events - %p.light - This URL will be triggered when a new tag is pushed to the repository + = form.check_box :tag_push_events, class: 'form-check-input' + = form.label :tag_push_events, class: 'list-label form-check-label ml-1' do + %strong Tag push events + %p.light.ml-1 + This URL will be triggered when a new tag is pushed to the repository %li - = form.check_box :note_events, class: 'float-left' - .prepend-left-20 - = form.label :note_events, class: 'list-label' do - %strong Comments - %p.light - This URL will be triggered when someone adds a comment + = form.check_box :note_events, class: 'form-check-input' + = form.label :note_events, class: 'list-label form-check-label ml-1' do + %strong Comments + %p.light.ml-1 + This URL will be triggered when someone adds a comment %li - = form.check_box :confidential_note_events, class: 'float-left' - .prepend-left-20 - = form.label :confidential_note_events, class: 'list-label' do - %strong Confidential Comments - %p.light - This URL will be triggered when someone adds a comment on a confidential issue + = form.check_box :confidential_note_events, class: 'form-check-input' + = form.label :confidential_note_events, class: 'list-label form-check-label ml-1' do + %strong Confidential Comments + %p.light.ml-1 + This URL will be triggered when someone adds a comment on a confidential issue %li - = form.check_box :issues_events, class: 'float-left' - .prepend-left-20 - = form.label :issues_events, class: 'list-label' do - %strong Issues events - %p.light - This URL will be triggered when an issue is created/updated/merged + = form.check_box :issues_events, class: 'form-check-input' + = form.label :issues_events, class: 'list-label form-check-label ml-1' do + %strong Issues events + %p.light.ml-1 + This URL will be triggered when an issue is created/updated/merged %li - = form.check_box :confidential_issues_events, class: 'float-left' - .prepend-left-20 - = form.label :confidential_issues_events, class: 'list-label' do - %strong Confidential Issues events - %p.light - This URL will be triggered when a confidential issue is created/updated/merged + = form.check_box :confidential_issues_events, class: 'form-check-input' + = form.label :confidential_issues_events, class: 'list-label form-check-label ml-1' do + %strong Confidential Issues events + %p.light.ml-1 + This URL will be triggered when a confidential issue is created/updated/merged %li - = form.check_box :merge_requests_events, class: 'float-left' - .prepend-left-20 - = form.label :merge_requests_events, class: 'list-label' do - %strong Merge request events - %p.light - This URL will be triggered when a merge request is created/updated/merged + = form.check_box :merge_requests_events, class: 'form-check-input' + = form.label :merge_requests_events, class: 'list-label form-check-label ml-1' do + %strong Merge request events + %p.light.ml-1 + This URL will be triggered when a merge request is created/updated/merged %li - = form.check_box :job_events, class: 'float-left' - .prepend-left-20 - = form.label :job_events, class: 'list-label' do - %strong Job events - %p.light - This URL will be triggered when the job status changes + = form.check_box :job_events, class: 'form-check-input' + = form.label :job_events, class: 'list-label form-check-label ml-1' do + %strong Job events + %p.light.ml-1 + This URL will be triggered when the job status changes %li - = form.check_box :pipeline_events, class: 'float-left' - .prepend-left-20 - = form.label :pipeline_events, class: 'list-label' do - %strong Pipeline events - %p.light - This URL will be triggered when the pipeline status changes + = form.check_box :pipeline_events, class: 'form-check-input' + = form.label :pipeline_events, class: 'list-label form-check-label ml-1' do + %strong Pipeline events + %p.light.ml-1 + This URL will be triggered when the pipeline status changes %li - = form.check_box :wiki_page_events, class: 'float-left' - .prepend-left-20 - = form.label :wiki_page_events, class: 'list-label' do - %strong Wiki Page events - %p.light - This URL will be triggered when a wiki page is created/updated + = form.check_box :wiki_page_events, class: 'form-check-input' + = form.label :wiki_page_events, class: 'list-label form-check-label ml-1' do + %strong Wiki Page events + %p.light.ml-1 + This URL will be triggered when a wiki page is created/updated .form-group = form.label :enable_ssl_verification, 'SSL verification', class: 'label-light checkbox' .form-check - = form.label :enable_ssl_verification do - = form.check_box :enable_ssl_verification + = form.check_box :enable_ssl_verification, class: 'form-check-input' + = form.label :enable_ssl_verification, class: 'form-check-label ml-1' do %strong Enable SSL verification diff --git a/changelogs/unreleased/25955-update-404-pages.yml b/changelogs/unreleased/25955-update-404-pages.yml new file mode 100644 index 00000000000..121229a77b9 --- /dev/null +++ b/changelogs/unreleased/25955-update-404-pages.yml @@ -0,0 +1,5 @@ +--- +title: Update 404 and 403 pages with helpful actions. +merge_request: 19096 +author: +type: changed diff --git a/changelogs/unreleased/41587-osw-mr-metrics-migration-cleanup.yml b/changelogs/unreleased/41587-osw-mr-metrics-migration-cleanup.yml new file mode 100644 index 00000000000..f953d380808 --- /dev/null +++ b/changelogs/unreleased/41587-osw-mr-metrics-migration-cleanup.yml @@ -0,0 +1,5 @@ +--- +title: Take two for MR metrics population background migration +merge_request: 19097 +author: +type: other diff --git a/changelogs/unreleased/44184-issues_ical_feed.yml b/changelogs/unreleased/44184-issues_ical_feed.yml new file mode 100644 index 00000000000..8151d82625a --- /dev/null +++ b/changelogs/unreleased/44184-issues_ical_feed.yml @@ -0,0 +1,5 @@ +--- +title: Export assigned issues in iCalendar feed +merge_request: 17783 +author: Imre Farkas +type: added diff --git a/changelogs/unreleased/45520-remove-links-from-web-ide.yml b/changelogs/unreleased/45520-remove-links-from-web-ide.yml new file mode 100644 index 00000000000..81d5c26992f --- /dev/null +++ b/changelogs/unreleased/45520-remove-links-from-web-ide.yml @@ -0,0 +1,5 @@ +--- +title: Change the IDE file buttons for an "Open in file view" button +merge_request: 19129 +author: Sam Beckham +type: changed diff --git a/changelogs/unreleased/46552-fixes-redundant-message-for-failure-reasons.yml b/changelogs/unreleased/46552-fixes-redundant-message-for-failure-reasons.yml new file mode 100644 index 00000000000..43427aaa242 --- /dev/null +++ b/changelogs/unreleased/46552-fixes-redundant-message-for-failure-reasons.yml @@ -0,0 +1,5 @@ +--- +title: Removes redundant script failure message from Job page +merge_request: 19138 +author: +type: changed diff --git a/changelogs/unreleased/46600-fix-gitlab-revision-when-not-in-git-repo.yml b/changelogs/unreleased/46600-fix-gitlab-revision-when-not-in-git-repo.yml deleted file mode 100644 index 1d0b11cfd2a..00000000000 --- a/changelogs/unreleased/46600-fix-gitlab-revision-when-not-in-git-repo.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Replace Gitlab::REVISION with Gitlab.revision and handle installations without - a .git directory -merge_request: 19125 -author: -type: fixed diff --git a/changelogs/unreleased/46844-update-awesome_print-to-1-8-0.yml b/changelogs/unreleased/46844-update-awesome_print-to-1-8-0.yml new file mode 100644 index 00000000000..e6dc9a6187b --- /dev/null +++ b/changelogs/unreleased/46844-update-awesome_print-to-1-8-0.yml @@ -0,0 +1,5 @@ +--- +title: Update awesome_print to 1.8.0 +merge_request: 19163 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/46903-osw-fix-permitted-params-filtering-on-merge-scheduling.yml b/changelogs/unreleased/46903-osw-fix-permitted-params-filtering-on-merge-scheduling.yml new file mode 100644 index 00000000000..b3c8c8e4045 --- /dev/null +++ b/changelogs/unreleased/46903-osw-fix-permitted-params-filtering-on-merge-scheduling.yml @@ -0,0 +1,5 @@ +--- +title: Adjust permitted params filtering on merge scheduling +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/46999-line-profiling-modal-width.yml b/changelogs/unreleased/46999-line-profiling-modal-width.yml new file mode 100644 index 00000000000..130f50d1ec0 --- /dev/null +++ b/changelogs/unreleased/46999-line-profiling-modal-width.yml @@ -0,0 +1,5 @@ +--- +title: Fix UI broken in line profiling modal due to Bootstrap 4 +merge_request: 19253 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/blackst0ne-squash-and-merge-in-gitlab-core-ce.yml b/changelogs/unreleased/blackst0ne-squash-and-merge-in-gitlab-core-ce.yml new file mode 100644 index 00000000000..e603c835b5e --- /dev/null +++ b/changelogs/unreleased/blackst0ne-squash-and-merge-in-gitlab-core-ce.yml @@ -0,0 +1,5 @@ +--- +title: Add `Squash and merge` to GitLab Core (CE) +merge_request: 18956 +author: "@blackst0ne" +type: added diff --git a/changelogs/unreleased/bump-kubeclient-version-3-1-0.yml b/changelogs/unreleased/bump-kubeclient-version-3-1-0.yml new file mode 100644 index 00000000000..24f240410b0 --- /dev/null +++ b/changelogs/unreleased/bump-kubeclient-version-3-1-0.yml @@ -0,0 +1,5 @@ +--- +title: Updates the version of kubeclient from 3.0 to 3.1.0 +merge_request: 19199 +author: +type: other diff --git a/changelogs/unreleased/dm-api-projects-members-preload.yml b/changelogs/unreleased/dm-api-projects-members-preload.yml new file mode 100644 index 00000000000..e04e7c37d13 --- /dev/null +++ b/changelogs/unreleased/dm-api-projects-members-preload.yml @@ -0,0 +1,6 @@ +--- +title: Only preload member records for the relevant projects/groups/user in projects + API +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/ensure-remote-mirror-columns-in-ce.yml b/changelogs/unreleased/ensure-remote-mirror-columns-in-ce.yml deleted file mode 100644 index 7617412431f..00000000000 --- a/changelogs/unreleased/ensure-remote-mirror-columns-in-ce.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix remote mirror database inconsistencies when upgrading from EE to CE -merge_request: 19196 -author: -type: fixed diff --git a/changelogs/unreleased/fix-bitbucket_import_anonymous.yml b/changelogs/unreleased/fix-bitbucket_import_anonymous.yml new file mode 100644 index 00000000000..6e214b3c957 --- /dev/null +++ b/changelogs/unreleased/fix-bitbucket_import_anonymous.yml @@ -0,0 +1,5 @@ +--- +title: Import bitbucket issues that are reported by an anonymous user +merge_request: 18199 +author: bartl +type: fixed diff --git a/changelogs/unreleased/fix-missing-timeout.yml b/changelogs/unreleased/fix-missing-timeout.yml new file mode 100644 index 00000000000..e0a61eb866c --- /dev/null +++ b/changelogs/unreleased/fix-missing-timeout.yml @@ -0,0 +1,5 @@ +--- +title: Missing timeout value in object storage pre-authorization +merge_request: 19201 +author: +type: fixed diff --git a/changelogs/unreleased/fix-nbsp-after-sign-in-with-google.yml b/changelogs/unreleased/fix-nbsp-after-sign-in-with-google.yml new file mode 100644 index 00000000000..73b478eff3e --- /dev/null +++ b/changelogs/unreleased/fix-nbsp-after-sign-in-with-google.yml @@ -0,0 +1,5 @@ +--- +title: Fix after sign-in with Google button +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fj-36819-remove-v3-api.yml b/changelogs/unreleased/fj-36819-remove-v3-api.yml new file mode 100644 index 00000000000..e5355252458 --- /dev/null +++ b/changelogs/unreleased/fj-36819-remove-v3-api.yml @@ -0,0 +1,5 @@ +--- +title: Removed API v3 from the codebase +merge_request: 18970 +author: +type: removed diff --git a/changelogs/unreleased/issue_45082.yml b/changelogs/unreleased/issue_45082.yml new file mode 100644 index 00000000000..b916a36c17b --- /dev/null +++ b/changelogs/unreleased/issue_45082.yml @@ -0,0 +1,5 @@ +--- +title: Add merge requests list endpoint for groups +merge_request: +author: +type: other diff --git a/changelogs/unreleased/mattermost-api-v4.yml b/changelogs/unreleased/mattermost-api-v4.yml new file mode 100644 index 00000000000..8c5033f2a0c --- /dev/null +++ b/changelogs/unreleased/mattermost-api-v4.yml @@ -0,0 +1,5 @@ +--- +title: Updated Mattermost integration to use API v4 and only allow creation of Mattermost slash commands in the current user's teams +merge_request: 19043 +author: Harrison Healey +type: changed diff --git a/changelogs/unreleased/memoize-database-version.yml b/changelogs/unreleased/memoize-database-version.yml deleted file mode 100644 index 575348a53a1..00000000000 --- a/changelogs/unreleased/memoize-database-version.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Memoize Gitlab::Database.version -merge_request: -author: -type: performance diff --git a/changelogs/unreleased/rails5-active-sup-subscriber.yml b/changelogs/unreleased/rails5-active-sup-subscriber.yml new file mode 100644 index 00000000000..439fa6f428e --- /dev/null +++ b/changelogs/unreleased/rails5-active-sup-subscriber.yml @@ -0,0 +1,5 @@ +--- +title: Make ActiveRecordSubscriber rails 5 compatible +merge_request: +author: +type: other diff --git a/changelogs/unreleased/rails5-fix-46230.yml b/changelogs/unreleased/rails5-fix-46230.yml new file mode 100644 index 00000000000..8ec28604483 --- /dev/null +++ b/changelogs/unreleased/rails5-fix-46230.yml @@ -0,0 +1,5 @@ +--- +title: Use strings as properties key in kubernetes service spec. +merge_request: 19265 +author: Jasper Maes +type: fixed diff --git a/changelogs/unreleased/sh-fix-issue-api-perf-n-plus-one.yml b/changelogs/unreleased/sh-fix-issue-api-perf-n-plus-one.yml new file mode 100644 index 00000000000..57ba081326f --- /dev/null +++ b/changelogs/unreleased/sh-fix-issue-api-perf-n-plus-one.yml @@ -0,0 +1,5 @@ +--- +title: Eliminate cached N+1 queries for projects in Issue API +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/sh-use-grape-path-helpers.yml b/changelogs/unreleased/sh-use-grape-path-helpers.yml new file mode 100644 index 00000000000..c462c7e8194 --- /dev/null +++ b/changelogs/unreleased/sh-use-grape-path-helpers.yml @@ -0,0 +1,5 @@ +--- +title: Replace grape-route-helpers with our own grape-path-helpers +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/winh-new-merge-request-encoding.yml b/changelogs/unreleased/winh-new-merge-request-encoding.yml deleted file mode 100644 index f797657e660..00000000000 --- a/changelogs/unreleased/winh-new-merge-request-encoding.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix encoding of branch names on compare and new merge request page -merge_request: 19143 -author: -type: fixed diff --git a/config/application.rb b/config/application.rb index 09f706e3d70..1b575f1325d 100644 --- a/config/application.rb +++ b/config/application.rb @@ -116,6 +116,7 @@ module Gitlab config.assets.precompile << "snippets.css" config.assets.precompile << "locale/**/app.js" config.assets.precompile << "emoji_sprites.css" + config.assets.precompile << "errors.css" # Import gitlab-svgs directly from vendored directory config.assets.paths << "#{config.root}/node_modules/@gitlab-org/gitlab-svgs/dist" diff --git a/config/initializers/console_message.rb b/config/initializers/console_message.rb index 2c46a25f365..f7c26732e6d 100644 --- a/config/initializers/console_message.rb +++ b/config/initializers/console_message.rb @@ -3,8 +3,8 @@ if defined?(Rails::Console) # note that this will not print out when using `spring` justify = 15 puts "-------------------------------------------------------------------------------------" - puts " Gitlab:".ljust(justify) + "#{Gitlab::VERSION} (#{Gitlab.revision})" - puts " Gitlab Shell:".ljust(justify) + Gitlab::Shell.new.version + puts " GitLab:".ljust(justify) + "#{Gitlab::VERSION} (#{Gitlab.revision})" + puts " GitLab Shell:".ljust(justify) + "#{Gitlab::VersionInfo.parse(Gitlab::Shell.new.version)}" puts " #{Gitlab::Database.adapter_name}:".ljust(justify) + Gitlab::Database.version puts "-------------------------------------------------------------------------------------" end diff --git a/config/initializers/grape_route_helpers_fix.rb b/config/initializers/grape_route_helpers_fix.rb deleted file mode 100644 index 612cca3dfbd..00000000000 --- a/config/initializers/grape_route_helpers_fix.rb +++ /dev/null @@ -1,51 +0,0 @@ -if defined?(GrapeRouteHelpers) - module GrapeRouteHelpers - module AllRoutes - # Bringing in PR https://github.com/reprah/grape-route-helpers/pull/21 due to abandonment. - # - # Without the following fix, when two helper methods are the same, but have different arguments - # (for example: api_v1_cats_owners_path(id: 1) vs api_v1_cats_owners_path(id: 1, owner_id: 2)) - # if the helper method with the least number of arguments is defined first (because the route was defined first) - # then it will shadow the longer route. - # - # The fix is to sort descending by amount of arguments - def decorated_routes - @decorated_routes ||= all_routes - .map { |r| DecoratedRoute.new(r) } - .sort_by { |r| -r.dynamic_path_segments.count } - end - end - - class DecoratedRoute - # GrapeRouteHelpers gem tries to parse the versions - # from a string, not supporting Grape `version` array definition. - # - # Without the following fix, we get this on route helpers generation: - # - # => undefined method `scan' for ["v3", "v4"] - # - # 2.0.0 implementation of this method: - # - # ``` - # def route_versions - # version_pattern = /[^\[",\]\s]+/ - # if route_version - # route_version.scan(version_pattern) - # else - # [nil] - # end - # end - # ``` - def route_versions - return [nil] if route_version.nil? || route_version.empty? - - if route_version.is_a?(String) - version_pattern = /[^\[",\]\s]+/ - route_version.scan(version_pattern) - else - route_version - end - end - end - end -end diff --git a/config/routes/dashboard.rb b/config/routes/dashboard.rb index d2437285cdf..f1e8c2b9d82 100644 --- a/config/routes/dashboard.rb +++ b/config/routes/dashboard.rb @@ -1,4 +1,5 @@ resource :dashboard, controller: 'dashboard', only: [] do + get :issues, action: :issues_calendar, constraints: lambda { |req| req.format == :ics } get :issues get :merge_requests get :activity diff --git a/config/routes/group.rb b/config/routes/group.rb index 7c4c3d370e0..b09eb3c1b5b 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -5,9 +5,10 @@ end constraints(::Constraints::GroupUrlConstrainer.new) do scope(path: 'groups/*id', controller: :groups, - constraints: { id: Gitlab::PathRegex.full_namespace_route_regex, format: /(html|json|atom)/ }) do + constraints: { id: Gitlab::PathRegex.full_namespace_route_regex, format: /(html|json|atom|ics)/ }) do scope(path: '-') do get :edit, as: :edit_group + get :issues, as: :issues_group_calendar, action: :issues_calendar, constraints: lambda { |req| req.format == :ics } get :issues, as: :issues_group get :merge_requests, as: :merge_requests_group get :projects, as: :projects_group @@ -30,6 +31,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do resource :variables, only: [:show, :update] resources :children, only: [:index] + resources :shared_projects, only: [:index] resources :labels, except: [:show] do post :toggle_subscription, on: :member diff --git a/config/routes/profile.rb b/config/routes/profile.rb index a9ba5ac2c0b..c1cac3905f1 100644 --- a/config/routes/profile.rb +++ b/config/routes/profile.rb @@ -7,7 +7,7 @@ resource :profile, only: [:show, :update] do get :applications, to: 'oauth/applications#index' put :reset_incoming_email_token - put :reset_rss_token + put :reset_feed_token put :update_username end diff --git a/config/routes/project.rb b/config/routes/project.rb index 5a1be1a8b73..6dfbd7ecd1f 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -353,6 +353,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end end + get :issues, to: 'issues#calendar', constraints: lambda { |req| req.format == :ics } resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do member do post :toggle_subscription diff --git a/db/migrate/20180408143354_rename_users_rss_token_to_feed_token.rb b/db/migrate/20180408143354_rename_users_rss_token_to_feed_token.rb new file mode 100644 index 00000000000..007cbebaf1b --- /dev/null +++ b/db/migrate/20180408143354_rename_users_rss_token_to_feed_token.rb @@ -0,0 +1,15 @@ +class RenameUsersRssTokenToFeedToken < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + rename_column_concurrently :users, :rss_token, :feed_token + end + + def down + cleanup_concurrent_column_rename :users, :feed_token, :rss_token + end +end diff --git a/db/migrate/20180515005612_add_squash_to_merge_requests.rb b/db/migrate/20180515005612_add_squash_to_merge_requests.rb new file mode 100644 index 00000000000..f526b45bd4b --- /dev/null +++ b/db/migrate/20180515005612_add_squash_to_merge_requests.rb @@ -0,0 +1,19 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddSquashToMergeRequests < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + DOWNTIME = false + + def up + unless column_exists?(:merge_requests, :squash) + add_column_with_default :merge_requests, :squash, :boolean, default: false, allow_null: false + end + end + + def down + remove_column :merge_requests, :squash if column_exists?(:merge_requests, :squash) + end +end diff --git a/db/post_migrate/20180408143355_cleanup_users_rss_token_rename.rb b/db/post_migrate/20180408143355_cleanup_users_rss_token_rename.rb new file mode 100644 index 00000000000..bff83379087 --- /dev/null +++ b/db/post_migrate/20180408143355_cleanup_users_rss_token_rename.rb @@ -0,0 +1,13 @@ +class CleanupUsersRssTokenRename < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + def up + cleanup_concurrent_column_rename :users, :rss_token, :feed_token + end + + def down + rename_column_concurrently :users, :feed_token, :rss_token + end +end diff --git a/db/post_migrate/20180521162137_migrate_remaining_mr_metrics_populating_background_migration.rb b/db/post_migrate/20180521162137_migrate_remaining_mr_metrics_populating_background_migration.rb new file mode 100644 index 00000000000..0282688fa40 --- /dev/null +++ b/db/post_migrate/20180521162137_migrate_remaining_mr_metrics_populating_background_migration.rb @@ -0,0 +1,44 @@ +class MigrateRemainingMrMetricsPopulatingBackgroundMigration < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + BATCH_SIZE = 5_000 + MIGRATION = 'PopulateMergeRequestMetricsWithEventsData' + DELAY_INTERVAL = 10.minutes + + disable_ddl_transaction! + + class MergeRequest < ActiveRecord::Base + self.table_name = 'merge_requests' + + include ::EachBatch + end + + def up + # Perform any ongoing background migration that might still be running. This + # avoids scheduling way too many of the same jobs on self-hosted instances + # if they're updating GitLab across multiple versions. The "Take one" + # migration was executed on 10.4 on + # SchedulePopulateMergeRequestMetricsWithEventsData. + Gitlab::BackgroundMigration.steal(MIGRATION) + + metrics_not_exists_clause = <<~SQL + NOT EXISTS (SELECT 1 FROM merge_request_metrics + WHERE merge_request_metrics.merge_request_id = merge_requests.id) + SQL + + relation = MergeRequest.where(metrics_not_exists_clause) + + # We currently have ~400_000 MR records without metrics on GitLab.com. + # This means it'll schedule ~80 jobs (5000 MRs each) with a 10 minutes gap, + # so this should take ~14 hours for all background migrations to complete. + # + queue_background_migration_jobs_by_range_at_intervals(relation, + MIGRATION, + DELAY_INTERVAL, + batch_size: BATCH_SIZE) + end + + def down + end +end diff --git a/db/schema.rb b/db/schema.rb index 359d25dafb5..53bdb084eea 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1219,6 +1219,7 @@ ActiveRecord::Schema.define(version: 20180529093006) do t.integer "latest_merge_request_diff_id" t.string "rebase_commit_sha" t.boolean "allow_maintainer_to_push" + t.boolean "squash", default: false, null: false end add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree @@ -2083,9 +2084,9 @@ ActiveRecord::Schema.define(version: 20180529093006) do t.date "last_activity_on" t.boolean "notified_of_own_activity" t.string "preferred_language" - t.string "rss_token" t.integer "theme_id", limit: 2 t.integer "accepted_term_id" + t.string "feed_token" end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree @@ -2093,12 +2094,12 @@ ActiveRecord::Schema.define(version: 20180529093006) do add_index "users", ["created_at"], name: "index_users_on_created_at", using: :btree add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree add_index "users", ["email"], name: "index_users_on_email_trigram", using: :gin, opclasses: {"email"=>"gin_trgm_ops"} + add_index "users", ["feed_token"], name: "index_users_on_feed_token", using: :btree add_index "users", ["ghost"], name: "index_users_on_ghost", using: :btree add_index "users", ["incoming_email_token"], name: "index_users_on_incoming_email_token", using: :btree add_index "users", ["name"], name: "index_users_on_name", using: :btree add_index "users", ["name"], name: "index_users_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"} add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree - add_index "users", ["rss_token"], name: "index_users_on_rss_token", using: :btree add_index "users", ["state"], name: "index_users_on_state", using: :btree add_index "users", ["username"], name: "index_users_on_username", using: :btree add_index "users", ["username"], name: "index_users_on_username_trigram", using: :gin, opclasses: {"username"=>"gin_trgm_ops"} diff --git a/doc/administration/custom_hooks.md b/doc/administration/custom_hooks.md index 960970aea30..1c508c77ffa 100644 --- a/doc/administration/custom_hooks.md +++ b/doc/administration/custom_hooks.md @@ -3,7 +3,7 @@ > **Note:** Custom Git hooks must be configured on the filesystem of the GitLab server. Only GitLab server administrators will be able to complete these tasks. -Please explore [webhooks] as an option if you do not +Please explore [webhooks] and [CI] as an option if you do not have filesystem access. For a user configurable Git hook interface, see [Push Rules](https://docs.gitlab.com/ee/push_rules/push_rules.html), available in GitLab Enterprise Edition. @@ -80,6 +80,7 @@ STDERR takes precedence over STDOUT. ![Custom message from custom Git hook](img/custom_hooks_error_msg.png) +[CI]: ../ci/README.md [hooks]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#Server-Side-Hooks [webhooks]: ../user/project/integrations/webhooks.md [5073]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5073 diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md index 957f17e3ea3..87e96b71dd4 100644 --- a/doc/administration/high_availability/nfs.md +++ b/doc/administration/high_availability/nfs.md @@ -25,7 +25,9 @@ options: errors when the Omnibus package tries to alter permissions. Note that GitLab and other bundled components do **not** run as `root` but as non-privileged users. The recommendation for `no_root_squash` is to allow the Omnibus package - to set ownership and permissions on files, as needed. + to set ownership and permissions on files, as needed. In some cases where the + `no_root_squash` option is not available, the `root` flag can achieve the same + result. - `sync` - Force synchronous behavior. Default is asynchronous and under certain circumstances it could lead to data loss if a failure occurs before data has synced. diff --git a/doc/administration/raketasks/storage.md b/doc/administration/raketasks/storage.md index 6ec5baeb6e3..cfd601b8866 100644 --- a/doc/administration/raketasks/storage.md +++ b/doc/administration/raketasks/storage.md @@ -24,7 +24,6 @@ gitlab-rake gitlab:storage:migrate_to_hashed ```bash rake gitlab:storage:migrate_to_hashed - ``` You can monitor the progress in the _Admin > Monitoring > Background jobs_ screen. @@ -52,7 +51,6 @@ gitlab-rake gitlab:storage:legacy_projects ```bash rake gitlab:storage:legacy_projects - ``` ------ @@ -86,7 +84,6 @@ gitlab-rake gitlab:storage:hashed_projects ```bash rake gitlab:storage:hashed_projects - ``` ------ @@ -120,7 +117,6 @@ gitlab-rake gitlab:storage:legacy_attachments ```bash rake gitlab:storage:legacy_attachments - ``` ------ @@ -137,7 +133,6 @@ gitlab-rake gitlab:storage:list_legacy_attachments ```bash rake gitlab:storage:list_legacy_attachments - ``` ## List attachments on Hashed storage @@ -154,7 +149,6 @@ gitlab-rake gitlab:storage:hashed_attachments ```bash rake gitlab:storage:hashed_attachments - ``` ------ @@ -171,7 +165,6 @@ gitlab-rake gitlab:storage:list_hashed_attachments ```bash rake gitlab:storage:list_hashed_attachments - ``` [storage-types]: ../repository_storage_types.md diff --git a/doc/api/README.md b/doc/api/README.md index 194907accc7..1c756dc855f 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -90,24 +90,23 @@ specification. ## Compatibility Guidelines The HTTP API is versioned using a single number, the current one being 4. This -number symbolises the same as the major version number as described by +number symbolises the same as the major version number as described by [SemVer](https://semver.org/). This mean that backward incompatible changes will require this version number to change. However, the minor version is -not explicit. This allows for a stable API endpoint, but also means new +not explicit. This allows for a stable API endpoint, but also means new features can be added to the API in the same version number. New features and bug fixes are released in tandem with a new GitLab, and apart from incidental patch and security releases, are released on the 22nd each -month. Backward incompatible changes (e.g. endpoints removal, parameters -removal etc.), as well as removal of entire API versions are done in tandem -with a major point release of GitLab itself. All deprecations and changes -between two versions should be listed in the documentation. For the changes +month. Backward incompatible changes (e.g. endpoints removal, parameters +removal etc.), as well as removal of entire API versions are done in tandem +with a major point release of GitLab itself. All deprecations and changes +between two versions should be listed in the documentation. For the changes between v3 and v4; please read the [v3 to v4 documentation](v3_to_v4.md) #### Current status -Currently two API versions are available, v3 and v4. v3 is deprecated and -will soon be removed. Deletion is scheduled for +Currently only API version v4 is available. Version v3 was removed in [GitLab 11.0](https://gitlab.com/gitlab-org/gitlab-ce/issues/36819). ## Basic usage diff --git a/doc/api/applications.md b/doc/api/applications.md index 933867ed0bb..6d244594b71 100644 --- a/doc/api/applications.md +++ b/doc/api/applications.md @@ -23,7 +23,7 @@ POST /applications | `scopes` | string | yes | The scopes of the application | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "name=MyApplication&redirect_uri=http://redirect.uri&scopes=" https://gitlab.example.com/api/v3/applications +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "name=MyApplication&redirect_uri=http://redirect.uri&scopes=" https://gitlab.example.com/api/v4/applications ``` Example response: diff --git a/doc/api/environments.md b/doc/api/environments.md index 6e20781f51a..29da4590a59 100644 --- a/doc/api/environments.md +++ b/doc/api/environments.md @@ -123,7 +123,7 @@ POST /projects/:id/environments/:environment_id/stop | `environment_id` | integer | yes | The ID of the environment | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments/1/stop" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/environments/1/stop" ``` Example response: diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 4e34831422a..051d2a10bc6 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -107,6 +107,7 @@ Parameters: "changes_count": "1", "should_remove_source_branch": true, "force_remove_source_branch": false, + "squash": false, "web_url": "http://example.com/example/example/merge_requests/1", "time_stats": { "time_estimate": 0, @@ -226,6 +227,113 @@ Parameters: "changes_count": "1", "should_remove_source_branch": true, "force_remove_source_branch": false, + "squash": false, + "web_url": "http://example.com/example/example/merge_requests/1", + "discussion_locked": false, + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null + } + } +] +``` + +## List group merge requests + +Get all merge requests for this group and its subgroups. +The `state` parameter can be used to get only merge requests with a given state (`opened`, `closed`, or `merged`) or all of them (`all`). +The pagination parameters `page` and `per_page` can be used to restrict the list of merge requests. + +``` +GET /groups/:id/merge_requests +GET /groups/:id/merge_requests?state=opened +GET /groups/:id/merge_requests?state=all +GET /groups/:id/merge_requests?milestone=release +GET /groups/:id/merge_requests?labels=bug,reproduced +GET /groups/:id/merge_requests?my_reaction_emoji=star +``` + +`group_id` represents the ID of the group which contains the project where the MR resides. + +Parameters: + +| Attribute | Type | Required | Description | +| ------------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ | +| `id` | integer | yes | The ID of a group | +| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, or `merged` | +| `order_by` | string | no | Return merge requests ordered by `created_at` or `updated_at` fields. Default is `created_at` | +| `sort` | string | no | Return merge requests sorted in `asc` or `desc` order. Default is `desc` | +| `milestone` | string | no | Return merge requests for a specific milestone | +| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request | +| `labels` | string | no | Return merge requests matching a comma separated list of labels | +| `created_after` | datetime | no | Return merge requests created on or after the given time | +| `created_before` | datetime | no | Return merge requests created on or before the given time | +| `updated_after` | datetime | no | Return merge requests updated on or after the given time | +| `updated_before` | datetime | no | Return merge requests updated on or before the given time | +| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> | +| `author_id` | integer | no | Returns merge requests created by the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ | +| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ | +| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ | +| `source_branch` | string | no | Return merge requests with the given source branch | +| `target_branch` | string | no | Return merge requests with the given target branch | +| `search` | string | no | Search merge requests against their `title` and `description` | + +```json +[ + { + "id": 1, + "iid": 1, + "target_branch": "master", + "source_branch": "test1", + "project_id": 3, + "title": "test1", + "state": "opened", + "created_at": "2017-04-29T08:46:00Z", + "updated_at": "2017-04-29T08:46:00Z", + "upvotes": 0, + "downvotes": 0, + "author": { + "id": 1, + "username": "admin", + "email": "admin@example.com", + "name": "Administrator", + "state": "active", + "created_at": "2012-04-29T08:46:00Z" + }, + "assignee": { + "id": 1, + "username": "admin", + "email": "admin@example.com", + "name": "Administrator", + "state": "active", + "created_at": "2012-04-29T08:46:00Z" + }, + "source_project_id": 2, + "target_project_id": 3, + "labels": [ ], + "description": "fixed login page css paddings", + "work_in_progress": false, + "milestone": { + "id": 5, + "iid": 1, + "project_id": 3, + "title": "v2.0", + "description": "Assumenda aut placeat expedita exercitationem labore sunt enim earum.", + "state": "closed", + "created_at": "2015-02-02T19:49:26.013Z", + "updated_at": "2015-02-02T19:49:26.013Z", + "due_date": null + }, + "merge_when_pipeline_succeeds": true, + "merge_status": "can_be_merged", + "sha": "8888888888888888888888888888888888888888", + "merge_commit_sha": null, + "user_notes_count": 1, + "changes_count": "1", + "should_remove_source_branch": true, + "force_remove_source_branch": false, "web_url": "http://example.com/example/example/merge_requests/1", "discussion_locked": false, "time_stats": { @@ -305,6 +413,7 @@ Parameters: "changes_count": "1", "should_remove_source_branch": true, "force_remove_source_branch": false, + "squash": false, "web_url": "http://example.com/example/example/merge_requests/1", "discussion_locked": false, "time_stats": { @@ -541,7 +650,8 @@ POST /projects/:id/merge_requests | `labels` | string | no | Labels for MR as a comma-separated list | | `milestone_id` | integer | no | The global ID of a milestone | | `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging | -| `allow_maintainer_to_push` | boolean | no | Whether or not a maintainer of the target project can push to the source branch | +| `allow_maintainer_to_push` | boolean | no | Whether or not a maintainer of the target project can push to the source branch | +| `squash` | boolean | no | Squash commits into a single commit when merging | ```json { @@ -595,6 +705,7 @@ POST /projects/:id/merge_requests "changes_count": "1", "should_remove_source_branch": true, "force_remove_source_branch": false, + "squash": false, "web_url": "http://example.com/example/example/merge_requests/1", "discussion_locked": false, "allow_maintainer_to_push": false, @@ -627,6 +738,7 @@ PUT /projects/:id/merge_requests/:merge_request_iid | `description` | string | no | Description of MR | | `state_event` | string | no | New state (close/reopen) | | `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging | +| `squash` | boolean | no | Squash commits into a single commit when merging | | `discussion_locked` | boolean | no | Flag indicating if the merge request's discussion is locked. If the discussion is locked only project members can add, edit or resolve comments. | | `allow_maintainer_to_push` | boolean | no | Whether or not a maintainer of the target project can push to the source branch | @@ -683,6 +795,7 @@ Must include at least one non-required attribute from above. "changes_count": "1", "should_remove_source_branch": true, "force_remove_source_branch": false, + "squash": false, "web_url": "http://example.com/example/example/merge_requests/1", "discussion_locked": false, "allow_maintainer_to_push": false, @@ -790,6 +903,7 @@ Parameters: "changes_count": "1", "should_remove_source_branch": true, "force_remove_source_branch": false, + "squash": false, "web_url": "http://example.com/example/example/merge_requests/1", "discussion_locked": false, "time_stats": { @@ -868,6 +982,7 @@ Parameters: "changes_count": "1", "should_remove_source_branch": true, "force_remove_source_branch": false, + "squash": false, "web_url": "http://example.com/example/example/merge_requests/1", "discussion_locked": false, "time_stats": { @@ -1200,6 +1315,7 @@ Example response: "changes_count": "1", "should_remove_source_branch": true, "force_remove_source_branch": false, + "squash": false, "web_url": "http://example.com/example/example/merge_requests/1" }, "target_url": "https://gitlab.example.com/gitlab-org/gitlab-ci/merge_requests/7", diff --git a/doc/api/projects.md b/doc/api/projects.md index 79cf5e1cc10..d3e95926322 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -1169,7 +1169,7 @@ The `file=` parameter must point to a file on your filesystem and be preceded by `@`. For example: ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "file=@dk.png" https://gitlab.example.com/api/v3/projects/5/uploads +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "file=@dk.png" https://gitlab.example.com/api/v4/projects/5/uploads ``` Returned object: diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md index 9835fab7c98..98eae66469f 100644 --- a/doc/api/v3_to_v4.md +++ b/doc/api/v3_to_v4.md @@ -2,10 +2,9 @@ Since GitLab 9.0, API V4 is the preferred version to be used. -API V3 will be unsupported from GitLab 9.5, to be released on August -22, 2017. It will be removed in GitLab 9.5 or later. In the meantime, we advise -you to make any necessary changes to applications that use V3. The V3 API -documentation is still +API V3 was unsupported from GitLab 9.5, released on August +22, 2017. API v3 was removed in [GitLab 11.0](https://gitlab.com/gitlab-org/gitlab-ce/issues/36819). +The V3 API documentation is still [available](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-16-stable/doc/api/README.md). Below are the changes made between V3 and V4. diff --git a/doc/development/code_review.md b/doc/development/code_review.md index d03b7fa23ca..23c80799235 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -22,7 +22,7 @@ There are a few rules to get your merge request accepted: 1. If your merge request includes UX, frontend and backend changes [^1], it must be **approved by a [UX team member, a frontend and a backend maintainer][team]**. 1. If your merge request includes a new dependency or a filesystem change, it must - be **approved by a [Build team member][team]**. See [how to work with the Build team][build handbook] for more details. + be *approved by a [Distribution team member][team]*. See how to work with the [Distribution team for more details.](https://about.gitlab.com/handbook/engineering/dev-backend/distribution/) 1. To lower the amount of merge requests maintainers need to review, you can ask or assign any [reviewers][projects] for a first review. 1. If you need some guidance (e.g. it's your first merge request), feel free diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md index 057a4094aed..7f061d06da8 100644 --- a/doc/development/ee_features.md +++ b/doc/development/ee_features.md @@ -368,27 +368,17 @@ resolve when you add the indentation to the equation. EE-specific views should be placed in `ee/app/views/`, using extra sub-directories if appropriate. +#### Using `render_if_exists` + Instead of using regular `render`, we should use `render_if_exists`, which will not render anything if it cannot find the specific partial. We use this so that we could put `render_if_exists` in CE, keeping code the same between CE and EE. -Also, it should search for the EE partial first, and then CE partial, and -then if nothing found, render nothing. - -This has two uses: - -- CE renders nothing, and EE renders its EE partial. -- CE renders its CE partial, and EE renders its EE partial, while the view - file stays the same. - The advantages of this: - Minimal code difference between CE and EE. - Very clear hints about where we're extending EE views while reading CE codes. -- Whenever we want to show something different in CE, we could just add CE - partials. Same applies the other way around. If we just use - `render_if_exists`, it would be very easy to change the content in EE. The disadvantage of this: @@ -396,6 +386,42 @@ The disadvantage of this: port `render_if_exists` to CE. - If we have typos in the partial name, it would be silently ignored. +#### Using `render_ce` + +For `render` and `render_if_exists`, they search for the EE partial first, +and then CE partial. They would only render a particular partial, not all +partials with the same name. We could take the advantage of this, so that +the same partial path (e.g. `shared/issuable/form/default_templates`) could +be referring to the CE partial in CE (i.e. +`app/views/shared/issuable/form/_default_templates.html.haml`), while EE +partial in EE (i.e. +`ee/app/views/shared/issuable/form/_default_templates.html.haml`). This way, +we could show different things between CE and EE. + +However sometimes we would also want to reuse the CE partial in EE partial +because we might just want to add something to the existing CE partial. We +could workaround this by adding another partial with a different name, but it +would be tedious to do so. + +In this case, we could as well just use `render_ce` which would ignore any EE +partials. One example would be +`ee/app/views/shared/issuable/form/_default_templates.html.haml`: + +``` haml +- if @project.feature_available?(:issuable_default_templates) + = render_ce 'shared/issuable/form/default_templates' +- elsif show_promotions? + = render 'shared/promotions/promote_issue_templates' +``` + +In the above example, we can't use +`render 'shared/issuable/form/default_templates'` because it would find the +same EE partial, causing infinite recursion. Instead, we could use `render_ce` +so it ignores any partials in `ee/` and then it would render the CE partial +(i.e. `app/views/shared/issuable/form/_default_templates.html.haml`) +for the same path (i.e. `shared/issuable/form/default_templates`). This way +we could easily wrap around the CE partial. + ### Code in `lib/` Place EE-specific logic in the top-level `EE` module namespace. Namespace the diff --git a/doc/development/new_fe_guide/tips.md b/doc/development/new_fe_guide/tips.md index f0cdf52d618..881ad1662ae 100644 --- a/doc/development/new_fe_guide/tips.md +++ b/doc/development/new_fe_guide/tips.md @@ -1,3 +1,9 @@ # Tips -> TODO: Add tips +## Clearing production compiled assets + +To clear production compiled assets created with `yarn webpack-prod` you can run: + +``` +yarn clean +``` diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md index a76a5096b69..1a926a660f1 100644 --- a/doc/development/testing_guide/best_practices.md +++ b/doc/development/testing_guide/best_practices.md @@ -120,6 +120,10 @@ Add `screenshot_and_save_page` in a `:js` spec to screenshot what Capybara Add `screenshot_and_open_image` in a `:js` spec to screenshot what Capybara "sees", and automatically open the image. +The HTML dumps created by this are missing CSS. +This results in them looking very different from the actual application. +There is a [small hack](https://gitlab.com/gitlab-org/gitlab-ce/snippets/1718469) to add CSS which makes debugging easier. + ### Fast unit tests Some classes are well-isolated from Rails and you should be able to test them diff --git a/doc/integration/shibboleth.md b/doc/integration/shibboleth.md index 8611d4f7315..0e43b4a39a4 100644 --- a/doc/integration/shibboleth.md +++ b/doc/integration/shibboleth.md @@ -107,7 +107,7 @@ you will not get a shibboleth session! RewriteEngine on #Don't escape encoded characters in api requests - RewriteCond %{REQUEST_URI} ^/api/v3/.* + RewriteCond %{REQUEST_URI} ^/api/v4/.* RewriteCond %{REQUEST_URI} !/Shibboleth.sso RewriteCond %{REQUEST_URI} !/shibboleth-sp RewriteRule .* http://127.0.0.1:8181%{REQUEST_URI} [P,QSA,NE] diff --git a/doc/user/project/index.md b/doc/user/project/index.md index 557375a1da9..e63ed88249d 100644 --- a/doc/user/project/index.md +++ b/doc/user/project/index.md @@ -72,6 +72,8 @@ website with GitLab Pages **Other features:** +- [Wiki](wiki/index.md): Document your GitLab project in an integrated Wiki +- [Snippets](../snippets.md): Store, share and collaborate on code snippets - [Cycle Analytics](cycle_analytics.md): Review your development lifecycle - [Syntax highlighting](highlighting.md): An alternative to customize your code blocks, overriding GitLab's default choice of language diff --git a/doc/user/project/issues/due_dates.md b/doc/user/project/issues/due_dates.md index 1bf8b776c2e..93306437c6c 100644 --- a/doc/user/project/issues/due_dates.md +++ b/doc/user/project/issues/due_dates.md @@ -39,5 +39,13 @@ The day before an open issue is due, an email will be sent to all participants of the issue. Both the due date and the day before are calculated using the server's timezone. +Issues with due dates can also be exported as an iCalendar feed. The URL of the +feed can be added to calendar applications. The feed is accessible by clicking +on the _Subscribe to calendar_ button on the following pages: +- on the **Assigned Issues** page that is linked on the right-hand side of the + GitLab header +- on the **Project Issues** page +- on the **Group Issues** page + [ce-3614]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3614 [permissions]: ../../permissions.md#project diff --git a/doc/user/project/merge_requests/img/squash_edit_form.png b/doc/user/project/merge_requests/img/squash_edit_form.png Binary files differnew file mode 100644 index 00000000000..496c6f44ea7 --- /dev/null +++ b/doc/user/project/merge_requests/img/squash_edit_form.png diff --git a/doc/user/project/merge_requests/img/squash_mr_commits.png b/doc/user/project/merge_requests/img/squash_mr_commits.png Binary files differnew file mode 100644 index 00000000000..5fc6a8c48bb --- /dev/null +++ b/doc/user/project/merge_requests/img/squash_mr_commits.png diff --git a/doc/user/project/merge_requests/img/squash_mr_widget.png b/doc/user/project/merge_requests/img/squash_mr_widget.png Binary files differnew file mode 100644 index 00000000000..9cb458b2a35 --- /dev/null +++ b/doc/user/project/merge_requests/img/squash_mr_widget.png diff --git a/doc/user/project/merge_requests/img/squash_squashed_commit.png b/doc/user/project/merge_requests/img/squash_squashed_commit.png Binary files differnew file mode 100644 index 00000000000..0cf5875f82c --- /dev/null +++ b/doc/user/project/merge_requests/img/squash_squashed_commit.png diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index 5932f5a2bc1..b75bcacc9d7 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -29,12 +29,12 @@ With GitLab merge requests, you can: - Enable [semi-linear history merge requests](#semi-linear-history-merge-requests) as another security layer to guarantee the pipeline is passing in the target branch - [Create new merge requests by email](#create-new-merge-requests-by-email) - Allow maintainers of the target project to push directly to the fork by [allowing edits from maintainers](maintainer_access.md) +- [Squash and merge](squash_and_merge.md) for a cleaner commit history With **[GitLab Enterprise Edition][ee]**, you can also: - View the deployment process across projects with [Multi-Project Pipeline Graphs](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html#multi-project-pipeline-graphs) **[PREMIUM]** - Request [approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your managers **[STARTER]** -- [Squash and merge](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html) for a cleaner commit history **[STARTER]** - Analyze the impact of your changes with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) **[STARTER]** ## Use cases @@ -57,7 +57,7 @@ B. Consider you're a web developer writing a webpage for your company's: 1. Your changes are previewed with [Review Apps](../../../ci/review_apps/index.md) 1. You request your web designers for their implementation 1. You request the [approval](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your manager **[STARTER]** -1. Once approved, your merge request is [squashed and merged](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html), and [deployed to staging with GitLab Pages](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/) (Squash and Merge is available in GitLab Starter) +1. Once approved, your merge request is [squashed and merged](squash_and_merge.md), and [deployed to staging with GitLab Pages](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/) 1. Your production team [cherry picks](#cherry-pick-changes) the merge commit into production ## Merge requests per project diff --git a/doc/user/project/merge_requests/squash_and_merge.md b/doc/user/project/merge_requests/squash_and_merge.md new file mode 100644 index 00000000000..a6efe893853 --- /dev/null +++ b/doc/user/project/merge_requests/squash_and_merge.md @@ -0,0 +1,80 @@ +# Squash and merge + +> [Introduced][ee-1024] in [GitLab Starter][ee] 8.17, and in [GitLab CE][ce] [11.0][ce-18956]. + +Combine all commits of your merge request into one and retain a clean history. + +## Overview + +Squashing lets you tidy up the commit history of a branch when accepting a merge +request. It applies all of the changes in the merge request as a single commit, +and then merges that commit using the merge method set for the project. + +In other words, squashing a merge request turns a long list of commits: + +![List of commits from a merge request][mr-commits] + +Into a single commit on merge: + +![A squashed commit followed by a merge commit][squashed-commit] + +The squashed commit's commit message is the merge request title. And note that +the squashed commit is still followed by a merge commit, as the merge +method for this example repository uses a merge commit. Squashing also works +with the fast-forward merge strategy, see +[squashing and fast-forward merge](#squash-and-fast-forward-merge) for more +details. + +## Use cases + +When working on a feature branch, you sometimes want to commit your current +progress, but don't really care about the commit messages. Those 'work in +progress commits' don't necessarily contain important information and as such +you'd rather not include them in your target branch. + +With squash and merge, when the merge request is ready to be merged, +all you have to do is enable squashing before you press merge to join +the commits include in the merge request into a single commit. + +This way, the history of your base branch remains clean with +meaningful commit messages and is simpler to [revert] if necessary. + +## Enabling squash for a merge request + +Anyone who can create or edit a merge request can choose for it to be squashed +on the merge request form: + +![Squash commits checkbox on edit form][squash-edit-form] + +--- + +This can then be overridden at the time of accepting the merge request: + +![Squash commits checkbox on accept merge request form][squash-mr-widget] + +## Commit metadata for squashed commits + +The squashed commit has the following metadata: + +* Message: the title of the merge request. +* Author: the author of the merge request. +* Committer: the user who initiated the squash. + +## Squash and fast-forward merge + +When a project has the [fast-forward merge setting enabled][ff-merge], the merge +request must be able to be fast-forwarded without squashing in order to squash +it. This is because squashing is only available when accepting a merge request, +so a merge request may need to be rebased before squashing, even though +squashing can itself be considered equivalent to rebasing. + +[ee-1024]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1024 +[ce-18956]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18956 +[mr-commits]: img/squash_mr_commits.png +[squashed-commit]: img/squash_squashed_commit.png +[squash-edit-form]: img/squash_edit_form.png +[squash-mr-widget]: img/squash_mr_widget.png +[ff-merge]: fast_forward_merge.md#enabling-fast-forward-merges +[ce]: https://about.gitlab.com/products/ +[ee]: https://about.gitlab.com/products/ +[revert]: revert_changes.md diff --git a/doc/user/snippets.md b/doc/user/snippets.md index 8397c0b00ef..7efb6bafee7 100644 --- a/doc/user/snippets.md +++ b/doc/user/snippets.md @@ -1,34 +1,51 @@ # Snippets -Snippets are little bits of code or text. +With GitLab Snippets you can store and share bits of code and text with other users. ![GitLab Snippet](img/gitlab_snippet.png) -There are 2 types of snippets - project snippets and personal snippets. +There are 2 types of snippets, personal snippets and project snippets. -## Comments - -With GitLab Snippets you engage in a conversation about that piece of code, -facilitating the collaboration among users. +## Personal snippets -> **Note:** -Comments on snippets was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/12910) in [GitLab Community Edition 9.2](https://about.gitlab.com/2017/05/22/gitlab-9-2-released/#comments-for-personal-snippets). +Personal snippets are not related to any project and can be created completely +independently. There are 3 visibility levels that can be set, public, internal +and private. See [Public access](../public_access/public_access.md) for more information. ## Project snippets -Project snippets are always related to a specific project - see [Project's features](project/index.md#project-39-s-features) for more information. +Project snippets are always related to a specific project. +See [Project's features](project/index.md#project-39-s-features) for more information. -## Personal snippets +## Discover snippets + +There are two main ways of how you can discover snippets in GitLab. -Personal snippets are not related to any project and can be created completely independently. There are 3 visibility levels that can be set (public, internal, private - see [Public Access](../public_access/public_access.md) for more information). +For exploring all snippets that are visible to you, you can go to the Snippets +dashboard of your GitLab instance via the top navigation. For GitLab.com you can +find it [here](https://gitlab.com/dashboard/snippets). This navigates you to an +overview that shows snippets you created and allows you to explore all snippets. + +If you want to discover snippets that belong to a specific project, you can navigate +to the Snippets page via the left side navigation on the project page. + +## Snippet comments + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/12910) in GitLab 9.2. + +With GitLab Snippets you engage in a conversation about that piece of code, +facilitating the collaboration among users. ## Downloading snippets You can download the raw content of a snippet. -By default snippets will be downloaded with Linux-style line endings (`LF`). If you want to preserve the original line endings you need to add a parameter `line_ending=raw` (eg. `https://gitlab.com/snippets/SNIPPET_ID/raw?line_ending=raw`). In case a snippet was created using the GitLab web interface the original line ending is Windows-like (`CRLF`). +By default snippets will be downloaded with Linux-style line endings (`LF`). If +you want to preserve the original line endings you need to add a parameter `line_ending=raw` +(e.g., `https://gitlab.com/snippets/SNIPPET_ID/raw?line_ending=raw`). In case a +snippet was created using the GitLab web interface the original line ending is Windows-like (`CRLF`). -## Embedded Snippets +## Embedded snippets > Introduced in GitLab 10.8. diff --git a/lib/api/api.rb b/lib/api/api.rb index 206fabe5c43..7ea575a9661 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -22,48 +22,14 @@ module API allow_access_with_scope :api prefix :api - version %w(v3 v4), using: :path - version 'v3', using: :path do - helpers ::API::V3::Helpers - helpers ::API::Helpers::CommonHelpers - - mount ::API::V3::AwardEmoji - mount ::API::V3::Boards - mount ::API::V3::Branches - mount ::API::V3::BroadcastMessages - mount ::API::V3::Builds - mount ::API::V3::Commits - mount ::API::V3::DeployKeys - mount ::API::V3::Environments - mount ::API::V3::Files - mount ::API::V3::Groups - mount ::API::V3::Issues - mount ::API::V3::Labels - mount ::API::V3::Members - mount ::API::V3::MergeRequestDiffs - mount ::API::V3::MergeRequests - mount ::API::V3::Notes - mount ::API::V3::Pipelines - mount ::API::V3::ProjectHooks - mount ::API::V3::Milestones - mount ::API::V3::Projects - mount ::API::V3::ProjectSnippets - mount ::API::V3::Repositories - mount ::API::V3::Runners - mount ::API::V3::Services - mount ::API::V3::Settings - mount ::API::V3::Snippets - mount ::API::V3::Subscriptions - mount ::API::V3::SystemHooks - mount ::API::V3::Tags - mount ::API::V3::Templates - mount ::API::V3::Todos - mount ::API::V3::Triggers - mount ::API::V3::Users - mount ::API::V3::Variables + route :any, '*path' do + error!('API V3 is no longer supported. Use API V4 instead.', 410) + end end + version 'v4', using: :path + before do header['X-Frame-Options'] = 'SAMEORIGIN' header['X-Content-Type-Options'] = 'nosniff' diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 3688b44b8df..c4537036a3a 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -568,6 +568,8 @@ module API expose :time_stats, using: 'API::Entities::IssuableTimeStats' do |merge_request| merge_request end + + expose :squash end class MergeRequest < MergeRequestBasic @@ -830,8 +832,8 @@ module API class ProjectWithAccess < Project expose :permissions do expose :project_access, using: Entities::ProjectAccess do |project, options| - if options.key?(:project_members) - (options[:project_members] || []).find { |member| member.source_id == project.id } + if options[:project_members] + options[:project_members].find { |member| member.source_id == project.id } else project.project_member(options[:current_user]) end @@ -839,8 +841,8 @@ module API expose :group_access, using: Entities::GroupAccess do |project, options| if project.group - if options.key?(:group_members) - (options[:group_members] || []).find { |member| member.source_id == project.namespace_id } + if options[:group_members] + options[:group_members].find { |member| member.source_id == project.namespace_id } else project.group.group_member(options[:current_user]) end @@ -851,13 +853,24 @@ module API def self.preload_relation(projects_relation, options = {}) relation = super(projects_relation, options) - unless options.key?(:group_members) - relation = relation.preload(group: [group_members: [:source, user: [notification_settings: :source]]]) + # MySQL doesn't support LIMIT inside an IN subquery + if Gitlab::Database.mysql? + project_ids = relation.pluck('projects.id') + namespace_ids = relation.pluck(:namespace_id) + else + project_ids = relation.select('projects.id') + namespace_ids = relation.select(:namespace_id) end - unless options.key?(:project_members) - relation = relation.preload(project_members: [:source, user: [notification_settings: :source]]) - end + options[:project_members] = options[:current_user] + .project_members + .where(source_id: project_ids) + .preload(:source, user: [notification_settings: :source]) + + options[:group_members] = options[:current_user] + .group_members + .where(source_id: namespace_ids) + .preload(:source, user: [notification_settings: :source]) relation end diff --git a/lib/api/helpers/related_resources_helpers.rb b/lib/api/helpers/related_resources_helpers.rb index a2cbed30229..bc7333ca4b3 100644 --- a/lib/api/helpers/related_resources_helpers.rb +++ b/lib/api/helpers/related_resources_helpers.rb @@ -1,7 +1,7 @@ module API module Helpers module RelatedResourcesHelpers - include GrapeRouteHelpers::NamedRouteMatcher + include GrapePathHelpers::NamedRouteMatcher def issues_available?(project, options) available?(:issues, project, options[:current_user]) diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 6d75e8817c4..b64f465ce56 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -16,7 +16,7 @@ module API args[:scope] = args[:scope].underscore if args[:scope] issues = IssuesFinder.new(current_user, args).execute - .preload(:assignees, :labels, :notes, :timelogs) + .preload(:assignees, :labels, :notes, :timelogs, :project) issues.reorder(args[:order_by] => args[:sort]) end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index bc4df16e3a8..b1e510d72de 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -10,12 +10,6 @@ module API helpers do params :optional_params_ee do end - - params :merge_params_ee do - end - - def update_merge_request_ee(merge_request) - end end def self.update_params_at_least_one_of @@ -29,6 +23,7 @@ module API target_branch title discussion_locked + squash ] end @@ -64,6 +59,18 @@ module API end end + def serializer_options_for(merge_requests) + options = { with: Entities::MergeRequestBasic, current_user: current_user } + + if params[:view] == 'simple' + options[:with] = Entities::MergeRequestSimple + else + options[:issuable_metadata] = issuable_meta_data(merge_requests, 'MergeRequest') + end + + options + end + params :merge_requests_params do optional :state, type: String, values: %w[opened closed merged all], default: 'all', desc: 'Return opened, closed, merged, or all merge requests' @@ -103,16 +110,26 @@ module API authenticate! unless params[:scope] == 'all' merge_requests = find_merge_requests - options = { with: Entities::MergeRequestBasic, - current_user: current_user } + present merge_requests, serializer_options_for(merge_requests) + end + end - if params[:view] == 'simple' - options[:with] = Entities::MergeRequestSimple - else - options[:issuable_metadata] = issuable_meta_data(merge_requests, 'MergeRequest') - end + params do + requires :id, type: String, desc: 'The ID of a group' + end + resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + desc 'Get a list of group merge requests' do + success Entities::MergeRequestBasic + end + params do + use :merge_requests_params + end + get ":id/merge_requests" do + group = find_group!(params[:id]) - present merge_requests, options + merge_requests = find_merge_requests(group_id: group.id, include_subgroups: true) + + present merge_requests, serializer_options_for(merge_requests) end end @@ -146,6 +163,7 @@ module API optional :labels, type: String, desc: 'Comma-separated list of label names' optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging' optional :allow_maintainer_to_push, type: Boolean, desc: 'Whether a maintainer of the target project can push to the source project' + optional :squash, type: Grape::API::Boolean, desc: 'When true, the commits will be squashed into a single commit on merge' use :optional_params_ee end @@ -163,15 +181,8 @@ module API merge_requests = find_merge_requests(project_id: user_project.id) - options = { with: Entities::MergeRequestBasic, - current_user: current_user, - project: user_project } - - if params[:view] == 'simple' - options[:with] = Entities::MergeRequestSimple - else - options[:issuable_metadata] = issuable_meta_data(merge_requests, 'MergeRequest') - end + options = serializer_options_for(merge_requests) + options[:project] = user_project present merge_requests, options end @@ -308,8 +319,7 @@ module API optional :merge_when_pipeline_succeeds, type: Boolean, desc: 'When true, this merge request will be merged when the pipeline succeeds' optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch' - - use :merge_params_ee + optional :squash, type: Grape::API::Boolean, desc: 'When true, the commits will be squashed into a single commit on merge' end put ':id/merge_requests/:merge_request_iid/merge' do Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42317') @@ -327,7 +337,7 @@ module API check_sha_param!(params, merge_request) - update_merge_request_ee(merge_request) + merge_request.update(squash: params[:squash]) if params[:squash] merge_params = { commit_message: params[:merge_commit_message], diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 8871792060b..3ef3680c5d9 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -58,16 +58,9 @@ module API projects = paginate(projects) projects, options = with_custom_attributes(projects, options) - if current_user - project_members = current_user.project_members.preload(:source, user: [notification_settings: :source]) - group_members = current_user.group_members.preload(:source, user: [notification_settings: :source]) - end - options = options.reverse_merge( with: current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails, statistics: params[:statistics], - project_members: project_members, - group_members: group_members, current_user: current_user ) options[:with] = Entities::BasicProjectDetails if params[:simple] diff --git a/lib/api/settings.rb b/lib/api/settings.rb index 4b506127c0e..02ef89f997f 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -129,6 +129,7 @@ module API optional :gitaly_timeout_default, type: Integer, desc: 'Default Gitaly timeout, in seconds. Set to 0 to disable timeouts.' optional :gitaly_timeout_medium, type: Integer, desc: 'Medium Gitaly timeout, in seconds. Set to 0 to disable timeouts.' optional :gitaly_timeout_fast, type: Integer, desc: 'Gitaly fast operation timeout, in seconds. Set to 0 to disable timeouts.' + optional :usage_ping_enabled, type: Boolean, desc: 'Every week GitLab will report license usage back to GitLab, Inc.' ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type| optional :"#{type}_key_restriction", diff --git a/lib/api/v3/award_emoji.rb b/lib/api/v3/award_emoji.rb deleted file mode 100644 index b96b2d70b12..00000000000 --- a/lib/api/v3/award_emoji.rb +++ /dev/null @@ -1,130 +0,0 @@ -module API - module V3 - class AwardEmoji < Grape::API - include PaginationParams - - before { authenticate! } - AWARDABLES = %w[issue merge_request snippet].freeze - - resource :projects, requirements: { id: %r{[^/]+} } do - AWARDABLES.each do |awardable_type| - awardable_string = awardable_type.pluralize - awardable_id_string = "#{awardable_type}_id" - - params do - requires :id, type: String, desc: 'The ID of a project' - requires :"#{awardable_id_string}", type: Integer, desc: "The ID of an Issue, Merge Request or Snippet" - end - - [ - ":id/#{awardable_string}/:#{awardable_id_string}/award_emoji", - ":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji" - ].each do |endpoint| - - desc 'Get a list of project +awardable+ award emoji' do - detail 'This feature was introduced in 8.9' - success Entities::AwardEmoji - end - params do - use :pagination - end - get endpoint do - if can_read_awardable? - awards = awardable.award_emoji - present paginate(awards), with: Entities::AwardEmoji - else - not_found!("Award Emoji") - end - end - - desc 'Get a specific award emoji' do - detail 'This feature was introduced in 8.9' - success Entities::AwardEmoji - end - params do - requires :award_id, type: Integer, desc: 'The ID of the award' - end - get "#{endpoint}/:award_id" do - if can_read_awardable? - present awardable.award_emoji.find(params[:award_id]), with: Entities::AwardEmoji - else - not_found!("Award Emoji") - end - end - - desc 'Award a new Emoji' do - detail 'This feature was introduced in 8.9' - success Entities::AwardEmoji - end - params do - requires :name, type: String, desc: 'The name of a award_emoji (without colons)' - end - post endpoint do - not_found!('Award Emoji') unless can_read_awardable? && can_award_awardable? - - award = awardable.create_award_emoji(params[:name], current_user) - - if award.persisted? - present award, with: Entities::AwardEmoji - else - not_found!("Award Emoji #{award.errors.messages}") - end - end - - desc 'Delete a +awardables+ award emoji' do - detail 'This feature was introduced in 8.9' - success Entities::AwardEmoji - end - params do - requires :award_id, type: Integer, desc: 'The ID of an award emoji' - end - delete "#{endpoint}/:award_id" do - award = awardable.award_emoji.find(params[:award_id]) - - unauthorized! unless award.user == current_user || current_user.admin? - - award.destroy - present award, with: Entities::AwardEmoji - end - end - end - end - - helpers do - def can_read_awardable? - can?(current_user, read_ability(awardable), awardable) - end - - def can_award_awardable? - awardable.user_can_award?(current_user, params[:name]) - end - - def awardable - @awardable ||= - begin - if params.include?(:note_id) - note_id = params.delete(:note_id) - - awardable.notes.find(note_id) - elsif params.include?(:issue_id) - user_project.issues.find(params[:issue_id]) - elsif params.include?(:merge_request_id) - user_project.merge_requests.find(params[:merge_request_id]) - else - user_project.snippets.find(params[:snippet_id]) - end - end - end - - def read_ability(awardable) - case awardable - when Note - read_ability(awardable.noteable) - else - :"read_#{awardable.class.to_s.underscore}" - end - end - end - end - end -end diff --git a/lib/api/v3/boards.rb b/lib/api/v3/boards.rb deleted file mode 100644 index 94acc67171e..00000000000 --- a/lib/api/v3/boards.rb +++ /dev/null @@ -1,72 +0,0 @@ -module API - module V3 - class Boards < Grape::API - before { authenticate! } - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Get all project boards' do - detail 'This feature was introduced in 8.13' - success ::API::Entities::Board - end - get ':id/boards' do - authorize!(:read_board, user_project) - present user_project.boards, with: ::API::Entities::Board - end - - params do - requires :board_id, type: Integer, desc: 'The ID of a board' - end - segment ':id/boards/:board_id' do - helpers do - def project_board - board = user_project.boards.first - - if params[:board_id] == board.id - board - else - not_found!('Board') - end - end - - def board_lists - project_board.lists.destroyable - end - end - - desc 'Get the lists of a project board' do - detail 'Does not include `done` list. This feature was introduced in 8.13' - success ::API::Entities::List - end - get '/lists' do - authorize!(:read_board, user_project) - present board_lists, with: ::API::Entities::List - end - - desc 'Delete a board list' do - detail 'This feature was introduced in 8.13' - success ::API::Entities::List - end - params do - requires :list_id, type: Integer, desc: 'The ID of a board list' - end - delete "/lists/:list_id" do - authorize!(:admin_list, user_project) - - list = board_lists.find(params[:list_id]) - - service = ::Boards::Lists::DestroyService.new(user_project, current_user) - - if service.execute(list) - present list, with: ::API::Entities::List - else - render_api_error!({ error: 'List could not be deleted!' }, 400) - end - end - end - end - end - end -end diff --git a/lib/api/v3/branches.rb b/lib/api/v3/branches.rb deleted file mode 100644 index 25176c5b38e..00000000000 --- a/lib/api/v3/branches.rb +++ /dev/null @@ -1,76 +0,0 @@ -require 'mime/types' - -module API - module V3 - class Branches < Grape::API - before { authenticate! } - before { authorize! :download_code, user_project } - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Get a project repository branches' do - success ::API::Entities::Branch - end - get ":id/repository/branches" do - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42276') - - repository = user_project.repository - branches = repository.branches.sort_by(&:name) - merged_branch_names = repository.merged_branch_names(branches.map(&:name)) - - present branches, with: ::API::Entities::Branch, project: user_project, merged_branch_names: merged_branch_names - end - - desc 'Delete a branch' - params do - requires :branch, type: String, desc: 'The name of the branch' - end - delete ":id/repository/branches/:branch", requirements: { branch: /.+/ } do - authorize_push_project - - result = DeleteBranchService.new(user_project, current_user) - .execute(params[:branch]) - - if result[:status] == :success - status(200) - { - branch_name: params[:branch] - } - else - render_api_error!(result[:message], result[:return_code]) - end - end - - desc 'Delete all merged branches' - delete ":id/repository/merged_branches" do - DeleteMergedBranchesService.new(user_project, current_user).async_execute - - status(200) - end - - desc 'Create branch' do - success ::API::Entities::Branch - end - params do - requires :branch_name, type: String, desc: 'The name of the branch' - requires :ref, type: String, desc: 'Create branch from commit sha or existing branch' - end - post ":id/repository/branches" do - authorize_push_project - result = CreateBranchService.new(user_project, current_user) - .execute(params[:branch_name], params[:ref]) - - if result[:status] == :success - present result[:branch], - with: ::API::Entities::Branch, - project: user_project - else - render_api_error!(result[:message], 400) - end - end - end - end - end -end diff --git a/lib/api/v3/broadcast_messages.rb b/lib/api/v3/broadcast_messages.rb deleted file mode 100644 index 417e4ad0b26..00000000000 --- a/lib/api/v3/broadcast_messages.rb +++ /dev/null @@ -1,31 +0,0 @@ -module API - module V3 - class BroadcastMessages < Grape::API - include PaginationParams - - before { authenticate! } - before { authenticated_as_admin! } - - resource :broadcast_messages do - helpers do - def find_message - BroadcastMessage.find(params[:id]) - end - end - - desc 'Delete a broadcast message' do - detail 'This feature was introduced in GitLab 8.12.' - success ::API::Entities::BroadcastMessage - end - params do - requires :id, type: Integer, desc: 'Broadcast message ID' - end - delete ':id' do - message = find_message - - present message.destroy, with: ::API::Entities::BroadcastMessage - end - end - end - end -end diff --git a/lib/api/v3/builds.rb b/lib/api/v3/builds.rb deleted file mode 100644 index b49448e1e67..00000000000 --- a/lib/api/v3/builds.rb +++ /dev/null @@ -1,250 +0,0 @@ -module API - module V3 - class Builds < Grape::API - include PaginationParams - - before { authenticate! } - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do - helpers do - params :optional_scope do - optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show', - values: %w(pending running failed success canceled skipped), - coerce_with: ->(scope) { - if scope.is_a?(String) - [scope] - elsif scope.is_a?(::Hash) - scope.values - else - ['unknown'] - end - } - end - end - - desc 'Get a project builds' do - success ::API::V3::Entities::Build - end - params do - use :optional_scope - use :pagination - end - get ':id/builds' do - builds = user_project.builds.order('id DESC') - builds = filter_builds(builds, params[:scope]) - - builds = builds.preload(:user, :job_artifacts_archive, :runner, pipeline: :project) - present paginate(builds), with: ::API::V3::Entities::Build - end - - desc 'Get builds for a specific commit of a project' do - success ::API::V3::Entities::Build - end - params do - requires :sha, type: String, desc: 'The SHA id of a commit' - use :optional_scope - use :pagination - end - get ':id/repository/commits/:sha/builds' do - authorize_read_builds! - - break not_found! unless user_project.commit(params[:sha]) - - pipelines = user_project.pipelines.where(sha: params[:sha]) - builds = user_project.builds.where(pipeline: pipelines).order('id DESC') - builds = filter_builds(builds, params[:scope]) - - present paginate(builds), with: ::API::V3::Entities::Build - end - - desc 'Get a specific build of a project' do - success ::API::V3::Entities::Build - end - params do - requires :build_id, type: Integer, desc: 'The ID of a build' - end - get ':id/builds/:build_id' do - authorize_read_builds! - - build = get_build!(params[:build_id]) - - present build, with: ::API::V3::Entities::Build - end - - desc 'Download the artifacts file from build' do - detail 'This feature was introduced in GitLab 8.5' - end - params do - requires :build_id, type: Integer, desc: 'The ID of a build' - end - get ':id/builds/:build_id/artifacts' do - authorize_read_builds! - - build = get_build!(params[:build_id]) - - present_carrierwave_file!(build.artifacts_file) - end - - desc 'Download the artifacts file from build' do - detail 'This feature was introduced in GitLab 8.10' - end - params do - requires :ref_name, type: String, desc: 'The ref from repository' - requires :job, type: String, desc: 'The name for the build' - end - get ':id/builds/artifacts/:ref_name/download', - requirements: { ref_name: /.+/ } do - authorize_read_builds! - - builds = user_project.latest_successful_builds_for(params[:ref_name]) - latest_build = builds.find_by!(name: params[:job]) - - present_carrierwave_file!(latest_build.artifacts_file) - end - - # TODO: We should use `present_disk_file!` and leave this implementation for backward compatibility (when build trace - # is saved in the DB instead of file). But before that, we need to consider how to replace the value of - # `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse. - desc 'Get a trace of a specific build of a project' - params do - requires :build_id, type: Integer, desc: 'The ID of a build' - end - get ':id/builds/:build_id/trace' do - authorize_read_builds! - - build = get_build!(params[:build_id]) - - header 'Content-Disposition', "infile; filename=\"#{build.id}.log\"" - content_type 'text/plain' - env['api.format'] = :binary - - trace = build.trace.raw - body trace - end - - desc 'Cancel a specific build of a project' do - success ::API::V3::Entities::Build - end - params do - requires :build_id, type: Integer, desc: 'The ID of a build' - end - post ':id/builds/:build_id/cancel' do - authorize_update_builds! - - build = get_build!(params[:build_id]) - authorize!(:update_build, build) - - build.cancel - - present build, with: ::API::V3::Entities::Build - end - - desc 'Retry a specific build of a project' do - success ::API::V3::Entities::Build - end - params do - requires :build_id, type: Integer, desc: 'The ID of a build' - end - post ':id/builds/:build_id/retry' do - authorize_update_builds! - - build = get_build!(params[:build_id]) - authorize!(:update_build, build) - break forbidden!('Build is not retryable') unless build.retryable? - - build = Ci::Build.retry(build, current_user) - - present build, with: ::API::V3::Entities::Build - end - - desc 'Erase build (remove artifacts and build trace)' do - success ::API::V3::Entities::Build - end - params do - requires :build_id, type: Integer, desc: 'The ID of a build' - end - post ':id/builds/:build_id/erase' do - authorize_update_builds! - - build = get_build!(params[:build_id]) - authorize!(:erase_build, build) - break forbidden!('Build is not erasable!') unless build.erasable? - - build.erase(erased_by: current_user) - present build, with: ::API::V3::Entities::Build - end - - desc 'Keep the artifacts to prevent them from being deleted' do - success ::API::V3::Entities::Build - end - params do - requires :build_id, type: Integer, desc: 'The ID of a build' - end - post ':id/builds/:build_id/artifacts/keep' do - authorize_update_builds! - - build = get_build!(params[:build_id]) - authorize!(:update_build, build) - break not_found!(build) unless build.artifacts? - - build.keep_artifacts! - - status 200 - present build, with: ::API::V3::Entities::Build - end - - desc 'Trigger a manual build' do - success ::API::V3::Entities::Build - detail 'This feature was added in GitLab 8.11' - end - params do - requires :build_id, type: Integer, desc: 'The ID of a Build' - end - post ":id/builds/:build_id/play" do - authorize_read_builds! - - build = get_build!(params[:build_id]) - authorize!(:update_build, build) - bad_request!("Unplayable Job") unless build.playable? - - build.play(current_user) - - status 200 - present build, with: ::API::V3::Entities::Build - end - end - - helpers do - def find_build(id) - user_project.builds.find_by(id: id.to_i) - end - - def get_build!(id) - find_build(id) || not_found! - end - - def filter_builds(builds, scope) - return builds if scope.nil? || scope.empty? - - available_statuses = ::CommitStatus::AVAILABLE_STATUSES - - unknown = scope - available_statuses - render_api_error!('Scope contains invalid value(s)', 400) unless unknown.empty? - - builds.where(status: available_statuses && scope) - end - - def authorize_read_builds! - authorize! :read_build, user_project - end - - def authorize_update_builds! - authorize! :update_build, user_project - end - end - end - end -end diff --git a/lib/api/v3/commits.rb b/lib/api/v3/commits.rb deleted file mode 100644 index 4f6ea8f502e..00000000000 --- a/lib/api/v3/commits.rb +++ /dev/null @@ -1,199 +0,0 @@ -require 'mime/types' - -module API - module V3 - class Commits < Grape::API - include PaginationParams - - before { authenticate! } - before { authorize! :download_code, user_project } - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do - desc 'Get a project repository commits' do - success ::API::Entities::Commit - end - params do - optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used' - optional :since, type: DateTime, desc: 'Only commits after or in this date will be returned' - optional :until, type: DateTime, desc: 'Only commits before or in this date will be returned' - optional :page, type: Integer, default: 0, desc: 'The page for pagination' - optional :per_page, type: Integer, default: 20, desc: 'The number of results per page' - optional :path, type: String, desc: 'The file path' - end - get ":id/repository/commits" do - ref = params[:ref_name] || user_project.try(:default_branch) || 'master' - offset = params[:page] * params[:per_page] - - commits = user_project.repository.commits(ref, - path: params[:path], - limit: params[:per_page], - offset: offset, - after: params[:since], - before: params[:until]) - - present commits, with: ::API::Entities::Commit - end - - desc 'Commit multiple file changes as one commit' do - success ::API::Entities::CommitDetail - detail 'This feature was introduced in GitLab 8.13' - end - params do - requires :branch_name, type: String, desc: 'The name of branch' - requires :commit_message, type: String, desc: 'Commit message' - requires :actions, type: Array[Hash], desc: 'Actions to perform in commit' - optional :author_email, type: String, desc: 'Author email for commit' - optional :author_name, type: String, desc: 'Author name for commit' - end - post ":id/repository/commits" do - authorize! :push_code, user_project - - attrs = declared_params.dup - branch = attrs.delete(:branch_name) - attrs.merge!(start_branch: branch, branch_name: branch) - - result = ::Files::MultiService.new(user_project, current_user, attrs).execute - - if result[:status] == :success - commit_detail = user_project.repository.commits(result[:result], limit: 1).first - present commit_detail, with: ::API::Entities::CommitDetail - else - render_api_error!(result[:message], 400) - end - end - - desc 'Get a specific commit of a project' do - success ::API::Entities::CommitDetail - failure [[404, 'Not Found']] - end - params do - requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag' - optional :stats, type: Boolean, default: true, desc: 'Include commit stats' - end - get ":id/repository/commits/:sha", requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do - commit = user_project.commit(params[:sha]) - - not_found! "Commit" unless commit - - present commit, with: ::API::Entities::CommitDetail, stats: params[:stats] - end - - desc 'Get the diff for a specific commit of a project' do - failure [[404, 'Not Found']] - end - params do - requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag' - end - get ":id/repository/commits/:sha/diff", requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do - commit = user_project.commit(params[:sha]) - - not_found! "Commit" unless commit - - commit.raw_diffs.to_a - end - - desc "Get a commit's comments" do - success ::API::Entities::CommitNote - failure [[404, 'Not Found']] - end - params do - use :pagination - requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag' - end - get ':id/repository/commits/:sha/comments', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do - commit = user_project.commit(params[:sha]) - - not_found! 'Commit' unless commit - notes = commit.notes.order(:created_at) - - present paginate(notes), with: ::API::Entities::CommitNote - end - - desc 'Cherry pick commit into a branch' do - detail 'This feature was introduced in GitLab 8.15' - success ::API::Entities::Commit - end - params do - requires :sha, type: String, desc: 'A commit sha to be cherry picked' - requires :branch, type: String, desc: 'The name of the branch' - end - post ':id/repository/commits/:sha/cherry_pick', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do - authorize! :push_code, user_project - - commit = user_project.commit(params[:sha]) - not_found!('Commit') unless commit - - branch = user_project.repository.find_branch(params[:branch]) - not_found!('Branch') unless branch - - commit_params = { - commit: commit, - start_branch: params[:branch], - branch_name: params[:branch] - } - - result = ::Commits::CherryPickService.new(user_project, current_user, commit_params).execute - - if result[:status] == :success - branch = user_project.repository.find_branch(params[:branch]) - present user_project.repository.commit(branch.dereferenced_target), with: ::API::Entities::Commit - else - render_api_error!(result[:message], 400) - end - end - - desc 'Post comment to commit' do - success ::API::Entities::CommitNote - end - params do - requires :sha, type: String, regexp: /\A\h{6,40}\z/, desc: "The commit's SHA" - requires :note, type: String, desc: 'The text of the comment' - optional :path, type: String, desc: 'The file path' - given :path do - requires :line, type: Integer, desc: 'The line number' - requires :line_type, type: String, values: %w(new old), default: 'new', desc: 'The type of the line' - end - end - post ':id/repository/commits/:sha/comments', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do - commit = user_project.commit(params[:sha]) - not_found! 'Commit' unless commit - - opts = { - note: params[:note], - noteable_type: 'Commit', - commit_id: commit.id - } - - if params[:path] - commit.raw_diffs(limits: false).each do |diff| - next unless diff.new_path == params[:path] - - lines = Gitlab::Diff::Parser.new.parse(diff.diff.each_line) - - lines.each do |line| - next unless line.new_pos == params[:line] && line.type == params[:line_type] - - break opts[:line_code] = Gitlab::Git.diff_line_code(diff.new_path, line.new_pos, line.old_pos) - end - - break if opts[:line_code] - end - - opts[:type] = LegacyDiffNote.name if opts[:line_code] - end - - note = ::Notes::CreateService.new(user_project, current_user, opts).execute - - if note.save - present note, with: ::API::Entities::CommitNote - else - render_api_error!("Failed to save note #{note.errors.messages}", 400) - end - end - end - end - end -end diff --git a/lib/api/v3/deploy_keys.rb b/lib/api/v3/deploy_keys.rb deleted file mode 100644 index 47e54ca85a5..00000000000 --- a/lib/api/v3/deploy_keys.rb +++ /dev/null @@ -1,143 +0,0 @@ -module API - module V3 - class DeployKeys < Grape::API - before { authenticate! } - - helpers do - def add_deploy_keys_project(project, attrs = {}) - project.deploy_keys_projects.create(attrs) - end - - def find_by_deploy_key(project, key_id) - project.deploy_keys_projects.find_by!(deploy_key: key_id) - end - end - - get "deploy_keys" do - authenticated_as_admin! - - keys = DeployKey.all - present keys, with: ::API::Entities::SSHKey - end - - params do - requires :id, type: String, desc: 'The ID of the project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - before { authorize_admin_project } - - %w(keys deploy_keys).each do |path| - desc "Get a specific project's deploy keys" do - success ::API::Entities::DeployKeysProject - end - get ":id/#{path}" do - keys = user_project.deploy_keys_projects.preload(:deploy_key) - - present keys, with: ::API::Entities::DeployKeysProject - end - - desc 'Get single deploy key' do - success ::API::Entities::DeployKeysProject - end - params do - requires :key_id, type: Integer, desc: 'The ID of the deploy key' - end - get ":id/#{path}/:key_id" do - key = find_by_deploy_key(user_project, params[:key_id]) - - present key, with: ::API::Entities::DeployKeysProject - end - - desc 'Add new deploy key to currently authenticated user' do - success ::API::Entities::DeployKeysProject - end - params do - requires :key, type: String, desc: 'The new deploy key' - requires :title, type: String, desc: 'The name of the deploy key' - optional :can_push, type: Boolean, desc: "Can deploy key push to the project's repository" - end - post ":id/#{path}" do - params[:key].strip! - - # Check for an existing key joined to this project - key = user_project.deploy_keys_projects - .joins(:deploy_key) - .find_by(keys: { key: params[:key] }) - - if key - present key, with: ::API::Entities::DeployKeysProject - break - end - - # Check for available deploy keys in other projects - key = current_user.accessible_deploy_keys.find_by(key: params[:key]) - if key - added_key = add_deploy_keys_project(user_project, deploy_key: key, can_push: !!params[:can_push]) - - present added_key, with: ::API::Entities::DeployKeysProject - break - end - - # Create a new deploy key - key_attributes = { can_push: !!params[:can_push], - deploy_key_attributes: declared_params.except(:can_push) } - key = add_deploy_keys_project(user_project, key_attributes) - - if key.valid? - present key, with: ::API::Entities::DeployKeysProject - else - render_validation_error!(key) - end - end - - desc 'Enable a deploy key for a project' do - detail 'This feature was added in GitLab 8.11' - success ::API::Entities::SSHKey - end - params do - requires :key_id, type: Integer, desc: 'The ID of the deploy key' - end - post ":id/#{path}/:key_id/enable" do - key = ::Projects::EnableDeployKeyService.new(user_project, - current_user, declared_params).execute - - if key - present key, with: ::API::Entities::SSHKey - else - not_found!('Deploy Key') - end - end - - desc 'Disable a deploy key for a project' do - detail 'This feature was added in GitLab 8.11' - success ::API::Entities::SSHKey - end - params do - requires :key_id, type: Integer, desc: 'The ID of the deploy key' - end - delete ":id/#{path}/:key_id/disable" do - key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id]) - key.destroy - - present key.deploy_key, with: ::API::Entities::SSHKey - end - - desc 'Delete deploy key for a project' do - success Key - end - params do - requires :key_id, type: Integer, desc: 'The ID of the deploy key' - end - delete ":id/#{path}/:key_id" do - key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id]) - if key - key.destroy - else - not_found!('Deploy Key') - end - end - end - end - end - end -end diff --git a/lib/api/v3/deployments.rb b/lib/api/v3/deployments.rb deleted file mode 100644 index 1d4972eda26..00000000000 --- a/lib/api/v3/deployments.rb +++ /dev/null @@ -1,43 +0,0 @@ -module API - module V3 - # Deployments RESTful API endpoints - class Deployments < Grape::API - include PaginationParams - - before { authenticate! } - - params do - requires :id, type: String, desc: 'The project ID' - end - resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Get all deployments of the project' do - detail 'This feature was introduced in GitLab 8.11.' - success ::API::V3::Deployments - end - params do - use :pagination - end - get ':id/deployments' do - authorize! :read_deployment, user_project - - present paginate(user_project.deployments), with: ::API::V3::Deployments - end - - desc 'Gets a specific deployment' do - detail 'This feature was introduced in GitLab 8.11.' - success ::API::V3::Deployments - end - params do - requires :deployment_id, type: Integer, desc: 'The deployment ID' - end - get ':id/deployments/:deployment_id' do - authorize! :read_deployment, user_project - - deployment = user_project.deployments.find(params[:deployment_id]) - - present deployment, with: ::API::V3::Deployments - end - end - end - end -end diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb deleted file mode 100644 index 68b4d7c3982..00000000000 --- a/lib/api/v3/entities.rb +++ /dev/null @@ -1,309 +0,0 @@ -module API - module V3 - module Entities - class ProjectSnippet < Grape::Entity - expose :id, :title, :file_name - expose :author, using: ::API::Entities::UserBasic - expose :updated_at, :created_at - expose(:expires_at) { |snippet| nil } - - expose :web_url do |snippet, options| - Gitlab::UrlBuilder.build(snippet) - end - end - - class Note < Grape::Entity - expose :id - expose :note, as: :body - expose :attachment_identifier, as: :attachment - expose :author, using: ::API::Entities::UserBasic - expose :created_at, :updated_at - expose :system?, as: :system - expose :noteable_id, :noteable_type - # upvote? and downvote? are deprecated, always return false - expose(:upvote?) { |note| false } - expose(:downvote?) { |note| false } - end - - class PushEventPayload < Grape::Entity - expose :commit_count, :action, :ref_type, :commit_from, :commit_to - expose :ref, :commit_title - end - - class Event < Grape::Entity - expose :project_id, :action_name - expose :target_id, :target_type, :author_id - expose :target_title - expose :created_at - expose :note, using: Entities::Note, if: ->(event, options) { event.note? } - expose :author, using: ::API::Entities::UserBasic, if: ->(event, options) { event.author } - - expose :push_event_payload, - as: :push_data, - using: PushEventPayload, - if: -> (event, _) { event.push? } - - expose :author_username do |event, options| - event.author&.username - end - end - - class AwardEmoji < Grape::Entity - expose :id - expose :name - expose :user, using: ::API::Entities::UserBasic - expose :created_at, :updated_at - expose :awardable_id, :awardable_type - end - - class Project < Grape::Entity - expose :id, :description, :default_branch, :tag_list - expose :public?, as: :public - expose :archived?, as: :archived - expose :visibility_level, :ssh_url_to_repo, :http_url_to_repo, :web_url - expose :owner, using: ::API::Entities::UserBasic, unless: ->(project, options) { project.group } - expose :name, :name_with_namespace - expose :path, :path_with_namespace - expose :resolve_outdated_diff_discussions - expose :container_registry_enabled - - # Expose old field names with the new permissions methods to keep API compatible - expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:current_user]) } - expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) } - expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) } - expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) } - expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) } - - expose :created_at, :last_activity_at - expose :shared_runners_enabled - expose :lfs_enabled?, as: :lfs_enabled - expose :creator_id - expose :namespace, using: 'API::Entities::Namespace' - expose :forked_from_project, using: ::API::Entities::BasicProjectDetails, if: lambda { |project, options| project.forked? } - expose :avatar_url do |user, options| - user.avatar_url(only_path: false) - end - expose :star_count, :forks_count - expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) && project.default_issues_tracker? } - expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } - expose :public_builds - expose :shared_with_groups do |project, options| - ::API::Entities::SharedGroup.represent(project.project_group_links.all, options) - end - expose :only_allow_merge_if_pipeline_succeeds, as: :only_allow_merge_if_build_succeeds - expose :request_access_enabled - expose :only_allow_merge_if_all_discussions_are_resolved - - expose :statistics, using: '::API::V3::Entities::ProjectStatistics', if: :statistics - end - - class ProjectWithAccess < Project - expose :permissions do - expose :project_access, using: ::API::Entities::ProjectAccess do |project, options| - project.project_members.find_by(user_id: options[:current_user].id) - end - - expose :group_access, using: ::API::Entities::GroupAccess do |project, options| - if project.group - project.group.group_members.find_by(user_id: options[:current_user].id) - end - end - end - end - - class MergeRequest < Grape::Entity - expose :id, :iid - expose(:project_id) { |entity| entity.project.id } - expose :title, :description - expose :state, :created_at, :updated_at - expose :target_branch, :source_branch - expose :upvotes, :downvotes - expose :author, :assignee, using: ::API::Entities::UserBasic - expose :source_project_id, :target_project_id - expose :label_names, as: :labels - expose :work_in_progress?, as: :work_in_progress - expose :milestone, using: ::API::Entities::Milestone - expose :merge_when_pipeline_succeeds, as: :merge_when_build_succeeds - expose :merge_status - expose :diff_head_sha, as: :sha - expose :merge_commit_sha - expose :subscribed do |merge_request, options| - merge_request.subscribed?(options[:current_user], options[:project]) - end - expose :user_notes_count - expose :should_remove_source_branch?, as: :should_remove_source_branch - expose :force_remove_source_branch?, as: :force_remove_source_branch - - expose :web_url do |merge_request, options| - Gitlab::UrlBuilder.build(merge_request) - end - end - - class Group < Grape::Entity - expose :id, :name, :path, :description, :visibility_level - expose :lfs_enabled?, as: :lfs_enabled - expose :avatar_url do |user, options| - user.avatar_url(only_path: false) - end - expose :web_url - expose :request_access_enabled - expose :full_name, :full_path - - if ::Group.supports_nested_groups? - expose :parent_id - end - - expose :statistics, if: :statistics do - with_options format_with: -> (value) { value.to_i } do - expose :storage_size - expose :repository_size - expose :lfs_objects_size - expose :build_artifacts_size - end - end - end - - class GroupDetail < Group - expose :projects, using: Entities::Project - expose :shared_projects, using: Entities::Project - end - - class ApplicationSetting < Grape::Entity - expose :id - expose :default_projects_limit - expose :signup_enabled - expose :password_authentication_enabled_for_web, as: :password_authentication_enabled - expose :password_authentication_enabled_for_web, as: :signin_enabled - expose :gravatar_enabled - expose :sign_in_text - expose :after_sign_up_text - expose :created_at - expose :updated_at - expose :home_page_url - expose :default_branch_protection - expose :restricted_visibility_levels - expose :max_attachment_size - expose :session_expire_delay - expose :default_project_visibility - expose :default_snippet_visibility - expose :default_group_visibility - expose :domain_whitelist - expose :domain_blacklist_enabled - expose :domain_blacklist - expose :user_oauth_applications - expose :after_sign_out_path - expose :container_registry_token_expire_delay - expose :repository_storage - expose :repository_storages - expose :koding_enabled - expose :koding_url - expose :plantuml_enabled - expose :plantuml_url - expose :terminal_max_session_time - end - - class Environment < ::API::Entities::EnvironmentBasic - expose :project, using: Entities::Project - end - - class Trigger < Grape::Entity - expose :token, :created_at, :updated_at, :last_used - expose :owner, using: ::API::Entities::UserBasic - end - - class TriggerRequest < Grape::Entity - expose :id, :variables - end - - class Build < Grape::Entity - expose :id, :status, :stage, :name, :ref, :tag, :coverage - expose :created_at, :started_at, :finished_at - expose :user, with: ::API::Entities::User - expose :artifacts_file, using: ::API::Entities::JobArtifactFile, if: -> (build, opts) { build.artifacts? } - expose :commit, with: ::API::Entities::Commit - expose :runner, with: ::API::Entities::Runner - expose :pipeline, with: ::API::Entities::PipelineBasic - end - - class BuildArtifactFile < Grape::Entity - expose :filename, :size - end - - class Deployment < Grape::Entity - expose :id, :iid, :ref, :sha, :created_at - expose :user, using: ::API::Entities::UserBasic - expose :environment, using: ::API::Entities::EnvironmentBasic - expose :deployable, using: Entities::Build - end - - class MergeRequestChanges < MergeRequest - expose :diffs, as: :changes, using: ::API::Entities::Diff do |compare, _| - compare.raw_diffs(limits: false).to_a - end - end - - class ProjectStatistics < Grape::Entity - expose :commit_count - expose :storage_size - expose :repository_size - expose :lfs_objects_size - expose :build_artifacts_size - end - - class ProjectService < Grape::Entity - expose :id, :title, :created_at, :updated_at, :active - expose :push_events, :issues_events, :confidential_issues_events - expose :merge_requests_events, :tag_push_events, :note_events - expose :pipeline_events - expose :job_events, as: :build_events - # Expose serialized properties - expose :properties do |service, options| - service.properties.slice(*service.api_field_names) - end - end - - class ProjectHook < ::API::Entities::Hook - expose :project_id, :issues_events, :confidential_issues_events - expose :merge_requests_events, :note_events, :pipeline_events - expose :wiki_page_events - expose :job_events, as: :build_events - end - - class ProjectEntity < Grape::Entity - expose :id, :iid - expose(:project_id) { |entity| entity&.project.try(:id) } - expose :title, :description - expose :state, :created_at, :updated_at - end - - class IssueBasic < ProjectEntity - expose :label_names, as: :labels - expose :milestone, using: ::API::Entities::Milestone - expose :assignees, :author, using: ::API::Entities::UserBasic - - expose :assignee, using: ::API::Entities::UserBasic do |issue, options| - issue.assignees.first - end - - expose :user_notes_count - expose :upvotes, :downvotes - expose :due_date - expose :confidential - - expose :web_url do |issue, options| - Gitlab::UrlBuilder.build(issue) - end - end - - class Issue < IssueBasic - unexpose :assignees - expose :assignee do |issue, options| - ::API::Entities::UserBasic.represent(issue.assignees.first, options) - end - expose :subscribed do |issue, options| - issue.subscribed?(options[:current_user], options[:project] || issue.project) - end - end - end - end -end diff --git a/lib/api/v3/environments.rb b/lib/api/v3/environments.rb deleted file mode 100644 index 6bb4e016a01..00000000000 --- a/lib/api/v3/environments.rb +++ /dev/null @@ -1,87 +0,0 @@ -module API - module V3 - class Environments < Grape::API - include ::API::Helpers::CustomValidators - include PaginationParams - - before { authenticate! } - - params do - requires :id, type: String, desc: 'The project ID' - end - resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Get all environments of the project' do - detail 'This feature was introduced in GitLab 8.11.' - success Entities::Environment - end - params do - use :pagination - end - get ':id/environments' do - authorize! :read_environment, user_project - - present paginate(user_project.environments), with: Entities::Environment - end - - desc 'Creates a new environment' do - detail 'This feature was introduced in GitLab 8.11.' - success Entities::Environment - end - params do - requires :name, type: String, desc: 'The name of the environment to be created' - optional :external_url, type: String, desc: 'URL on which this deployment is viewable' - optional :slug, absence: { message: "is automatically generated and cannot be changed" } - end - post ':id/environments' do - authorize! :create_environment, user_project - - environment = user_project.environments.create(declared_params) - - if environment.persisted? - present environment, with: Entities::Environment - else - render_validation_error!(environment) - end - end - - desc 'Updates an existing environment' do - detail 'This feature was introduced in GitLab 8.11.' - success Entities::Environment - end - params do - requires :environment_id, type: Integer, desc: 'The environment ID' - optional :name, type: String, desc: 'The new environment name' - optional :external_url, type: String, desc: 'The new URL on which this deployment is viewable' - optional :slug, absence: { message: "is automatically generated and cannot be changed" } - end - put ':id/environments/:environment_id' do - authorize! :update_environment, user_project - - environment = user_project.environments.find(params[:environment_id]) - - update_params = declared_params(include_missing: false).extract!(:name, :external_url) - if environment.update(update_params) - present environment, with: Entities::Environment - else - render_validation_error!(environment) - end - end - - desc 'Deletes an existing environment' do - detail 'This feature was introduced in GitLab 8.11.' - success Entities::Environment - end - params do - requires :environment_id, type: Integer, desc: 'The environment ID' - end - delete ':id/environments/:environment_id' do - authorize! :update_environment, user_project - - environment = user_project.environments.find(params[:environment_id]) - - present environment.destroy, with: Entities::Environment - end - end - end - end -end diff --git a/lib/api/v3/files.rb b/lib/api/v3/files.rb deleted file mode 100644 index 7b4b3448b6d..00000000000 --- a/lib/api/v3/files.rb +++ /dev/null @@ -1,138 +0,0 @@ -module API - module V3 - class Files < Grape::API - helpers do - def commit_params(attrs) - { - file_path: attrs[:file_path], - start_branch: attrs[:branch], - branch_name: attrs[:branch], - commit_message: attrs[:commit_message], - file_content: attrs[:content], - file_content_encoding: attrs[:encoding], - author_email: attrs[:author_email], - author_name: attrs[:author_name] - } - end - - def commit_response(attrs) - { - file_path: attrs[:file_path], - branch: attrs[:branch] - } - end - - params :simple_file_params do - requires :file_path, type: String, desc: 'The path to new file. Ex. lib/class.rb' - requires :branch_name, type: String, desc: 'The name of branch' - requires :commit_message, type: String, desc: 'Commit Message' - optional :author_email, type: String, desc: 'The email of the author' - optional :author_name, type: String, desc: 'The name of the author' - end - - params :extended_file_params do - use :simple_file_params - requires :content, type: String, desc: 'File content' - optional :encoding, type: String, values: %w[base64], desc: 'File encoding' - end - end - - params do - requires :id, type: String, desc: 'The project ID' - end - resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Get a file from repository' - params do - requires :file_path, type: String, desc: 'The path to the file. Ex. lib/class.rb' - requires :ref, type: String, desc: 'The name of branch, tag, or commit' - end - get ":id/repository/files" do - authorize! :download_code, user_project - - commit = user_project.commit(params[:ref]) - not_found!('Commit') unless commit - - repo = user_project.repository - blob = repo.blob_at(commit.sha, params[:file_path]) - not_found!('File') unless blob - - blob.load_all_data! - status(200) - - { - file_name: blob.name, - file_path: blob.path, - size: blob.size, - encoding: "base64", - content: Base64.strict_encode64(blob.data), - ref: params[:ref], - blob_id: blob.id, - commit_id: commit.id, - last_commit_id: repo.last_commit_id_for_path(commit.sha, params[:file_path]) - } - end - - desc 'Create new file in repository' - params do - use :extended_file_params - end - post ":id/repository/files" do - authorize! :push_code, user_project - - file_params = declared_params(include_missing: false) - file_params[:branch] = file_params.delete(:branch_name) - - result = ::Files::CreateService.new(user_project, current_user, commit_params(file_params)).execute - - if result[:status] == :success - status(201) - commit_response(file_params) - else - render_api_error!(result[:message], 400) - end - end - - desc 'Update existing file in repository' - params do - use :extended_file_params - end - put ":id/repository/files" do - authorize! :push_code, user_project - - file_params = declared_params(include_missing: false) - file_params[:branch] = file_params.delete(:branch_name) - - result = ::Files::UpdateService.new(user_project, current_user, commit_params(file_params)).execute - - if result[:status] == :success - status(200) - commit_response(file_params) - else - http_status = result[:http_status] || 400 - render_api_error!(result[:message], http_status) - end - end - - desc 'Delete an existing file in repository' - params do - use :simple_file_params - end - delete ":id/repository/files" do - authorize! :push_code, user_project - - file_params = declared_params(include_missing: false) - file_params[:branch] = file_params.delete(:branch_name) - - result = ::Files::DeleteService.new(user_project, current_user, commit_params(file_params)).execute - - if result[:status] == :success - status(200) - commit_response(file_params) - else - render_api_error!(result[:message], 400) - end - end - end - end - end -end diff --git a/lib/api/v3/groups.rb b/lib/api/v3/groups.rb deleted file mode 100644 index 4fa7d196e50..00000000000 --- a/lib/api/v3/groups.rb +++ /dev/null @@ -1,187 +0,0 @@ -module API - module V3 - class Groups < Grape::API - include PaginationParams - - before { authenticate! } - - helpers do - params :optional_params do - optional :description, type: String, desc: 'The description of the group' - optional :visibility_level, type: Integer, desc: 'The visibility level of the group' - optional :lfs_enabled, type: Boolean, desc: 'Enable/disable LFS for the projects in this group' - optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access' - end - - params :statistics_params do - optional :statistics, type: Boolean, default: false, desc: 'Include project statistics' - end - - def present_groups(groups, options = {}) - options = options.reverse_merge( - with: Entities::Group, - current_user: current_user - ) - - groups = groups.with_statistics if options[:statistics] - present paginate(groups), options - end - end - - resource :groups do - desc 'Get a groups list' do - success Entities::Group - end - params do - use :statistics_params - optional :skip_groups, type: Array[Integer], desc: 'Array of group ids to exclude from list' - optional :all_available, type: Boolean, desc: 'Show all group that you have access to' - optional :search, type: String, desc: 'Search for a specific group' - optional :order_by, type: String, values: %w[name path], default: 'name', desc: 'Order by name or path' - optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)' - use :pagination - end - get do - groups = if current_user.admin - Group.all - elsif params[:all_available] - GroupsFinder.new(current_user).execute - else - current_user.groups - end - - groups = groups.search(params[:search]) if params[:search].present? - groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present? - groups = groups.reorder(params[:order_by] => params[:sort]) - - present_groups groups, statistics: params[:statistics] && current_user.admin? - end - - desc 'Get list of owned groups for authenticated user' do - success Entities::Group - end - params do - use :pagination - use :statistics_params - end - get '/owned' do - present_groups current_user.owned_groups, statistics: params[:statistics] - end - - desc 'Create a group. Available only for users who can create groups.' do - success Entities::Group - end - params do - requires :name, type: String, desc: 'The name of the group' - requires :path, type: String, desc: 'The path of the group' - - if ::Group.supports_nested_groups? - optional :parent_id, type: Integer, desc: 'The parent group id for creating nested group' - end - - use :optional_params - end - post do - authorize! :create_group - - group = ::Groups::CreateService.new(current_user, declared_params(include_missing: false)).execute - - if group.persisted? - present group, with: Entities::Group, current_user: current_user - else - render_api_error!("Failed to save group #{group.errors.messages}", 400) - end - end - end - - params do - requires :id, type: String, desc: 'The ID of a group' - end - resource :groups, requirements: { id: %r{[^/]+} } do - desc 'Update a group. Available only for users who can administrate groups.' do - success Entities::Group - end - params do - optional :name, type: String, desc: 'The name of the group' - optional :path, type: String, desc: 'The path of the group' - use :optional_params - at_least_one_of :name, :path, :description, :visibility_level, - :lfs_enabled, :request_access_enabled - end - put ':id' do - group = find_group!(params[:id]) - authorize! :admin_group, group - - if ::Groups::UpdateService.new(group, current_user, declared_params(include_missing: false)).execute - present group, with: Entities::GroupDetail, current_user: current_user - else - render_validation_error!(group) - end - end - - desc 'Get a single group, with containing projects.' do - success Entities::GroupDetail - end - get ":id" do - group = find_group!(params[:id]) - present group, with: Entities::GroupDetail, current_user: current_user - end - - desc 'Remove a group.' - delete ":id" do - group = find_group!(params[:id]) - authorize! :admin_group, group - ::Groups::DestroyService.new(group, current_user).async_execute - - accepted! - end - - desc 'Get a list of projects in this group.' do - success Entities::Project - end - params do - optional :archived, type: Boolean, default: false, desc: 'Limit by archived status' - optional :visibility, type: String, values: %w[public internal private], - desc: 'Limit by visibility' - optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria' - optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at], - default: 'created_at', desc: 'Return projects ordered by field' - optional :sort, type: String, values: %w[asc desc], default: 'desc', - desc: 'Return projects sorted in ascending and descending order' - optional :simple, type: Boolean, default: false, - desc: 'Return only the ID, URL, name, and path of each project' - optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user' - optional :starred, type: Boolean, default: false, desc: 'Limit by starred status' - - use :pagination - end - get ":id/projects" do - group = find_group!(params[:id]) - projects = GroupProjectsFinder.new(group: group, current_user: current_user).execute - projects = filter_projects(projects) - entity = params[:simple] ? ::API::Entities::BasicProjectDetails : Entities::Project - present paginate(projects), with: entity, current_user: current_user - end - - desc 'Transfer a project to the group namespace. Available only for admin.' do - success Entities::GroupDetail - end - params do - requires :project_id, type: String, desc: 'The ID or path of the project' - end - post ":id/projects/:project_id", requirements: { project_id: /.+/ } do - authenticated_as_admin! - group = find_group!(params[:id]) - project = find_project!(params[:project_id]) - result = ::Projects::TransferService.new(project, current_user).execute(group) - - if result - present group, with: Entities::GroupDetail, current_user: current_user - else - render_api_error!("Failed to transfer project #{project.errors.messages}", 400) - end - end - end - end - end -end diff --git a/lib/api/v3/helpers.rb b/lib/api/v3/helpers.rb deleted file mode 100644 index 4e63aa01c1a..00000000000 --- a/lib/api/v3/helpers.rb +++ /dev/null @@ -1,49 +0,0 @@ -module API - module V3 - module Helpers - def find_project_issue(id) - IssuesFinder.new(current_user, project_id: user_project.id).find(id) - end - - def find_project_merge_request(id) - MergeRequestsFinder.new(current_user, project_id: user_project.id).find(id) - end - - def find_merge_request_with_access(id, access_level = :read_merge_request) - merge_request = user_project.merge_requests.find(id) - authorize! access_level, merge_request - merge_request - end - - # project helpers - - def filter_projects(projects) - if params[:membership] - projects = projects.merge(current_user.authorized_projects) - end - - if params[:owned] - projects = projects.merge(current_user.owned_projects) - end - - if params[:starred] - projects = projects.merge(current_user.starred_projects) - end - - if params[:search].present? - projects = projects.search(params[:search]) - end - - if params[:visibility].present? - projects = projects.where(visibility_level: Gitlab::VisibilityLevel.level_value(params[:visibility])) - end - - unless params[:archived].nil? - projects = projects.where(archived: to_boolean(params[:archived])) - end - - projects.reorder(params[:order_by] => params[:sort]) - end - end - end -end diff --git a/lib/api/v3/issues.rb b/lib/api/v3/issues.rb deleted file mode 100644 index b59947d81d9..00000000000 --- a/lib/api/v3/issues.rb +++ /dev/null @@ -1,240 +0,0 @@ -module API - module V3 - class Issues < Grape::API - include PaginationParams - - before { authenticate! } - - helpers do - def find_issues(args = {}) - args = params.merge(args) - args = convert_parameters_from_legacy_format(args) - - args.delete(:id) - args[:milestone_title] = args.delete(:milestone) - - match_all_labels = args.delete(:match_all_labels) - labels = args.delete(:labels) - args[:label_name] = labels if match_all_labels - - # IssuesFinder expects iids - args[:iids] = args.delete(:iid) if args.key?(:iid) - - issues = IssuesFinder.new(current_user, args).execute.inc_notes_with_associations - - if !match_all_labels && labels.present? - issues = issues.includes(:labels).where('labels.title' => labels.split(',')) - end - - issues.reorder(args[:order_by] => args[:sort]) - end - - params :issues_params do - optional :labels, type: String, desc: 'Comma-separated list of label names' - optional :milestone, type: String, desc: 'Milestone title' - optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at', - desc: 'Return issues ordered by `created_at` or `updated_at` fields.' - optional :sort, type: String, values: %w[asc desc], default: 'desc', - desc: 'Return issues sorted in `asc` or `desc` order.' - optional :milestone, type: String, desc: 'Return issues for a specific milestone' - use :pagination - end - - params :issue_params do - optional :description, type: String, desc: 'The description of an issue' - optional :assignee_id, type: Integer, desc: 'The ID of a user to assign issue' - optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign issue' - optional :labels, type: String, desc: 'Comma-separated list of label names' - optional :due_date, type: String, desc: 'Date time string in the format YEAR-MONTH-DAY' - optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential' - end - end - - resource :issues do - desc "Get currently authenticated user's issues" do - success ::API::V3::Entities::Issue - end - params do - optional :state, type: String, values: %w[opened closed all], default: 'all', - desc: 'Return opened, closed, or all issues' - use :issues_params - end - get do - issues = find_issues(scope: 'authored') - - present paginate(issues), with: ::API::V3::Entities::Issue, current_user: current_user - end - end - - params do - requires :id, type: String, desc: 'The ID of a group' - end - resource :groups, requirements: { id: %r{[^/]+} } do - desc 'Get a list of group issues' do - success ::API::V3::Entities::Issue - end - params do - optional :state, type: String, values: %w[opened closed all], default: 'all', - desc: 'Return opened, closed, or all issues' - use :issues_params - end - get ":id/issues" do - group = find_group!(params[:id]) - - issues = find_issues(group_id: group.id, match_all_labels: true) - - present paginate(issues), with: ::API::V3::Entities::Issue, current_user: current_user - end - end - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - include TimeTrackingEndpoints - - desc 'Get a list of project issues' do - detail 'iid filter is deprecated have been removed on V4' - success ::API::V3::Entities::Issue - end - params do - optional :state, type: String, values: %w[opened closed all], default: 'all', - desc: 'Return opened, closed, or all issues' - optional :iid, type: Integer, desc: 'Return the issue having the given `iid`' - use :issues_params - end - get ":id/issues" do - project = find_project!(params[:id]) - - issues = find_issues(project_id: project.id) - - present paginate(issues), with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project - end - - desc 'Get a single project issue' do - success ::API::V3::Entities::Issue - end - params do - requires :issue_id, type: Integer, desc: 'The ID of a project issue' - end - get ":id/issues/:issue_id" do - issue = find_project_issue(params[:issue_id]) - present issue, with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project - end - - desc 'Create a new project issue' do - success ::API::V3::Entities::Issue - end - params do - requires :title, type: String, desc: 'The title of an issue' - optional :created_at, type: DateTime, - desc: 'Date time when the issue was created. Available only for admins and project owners.' - optional :merge_request_for_resolving_discussions, type: Integer, - desc: 'The IID of a merge request for which to resolve discussions' - use :issue_params - end - post ':id/issues' do - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42131') - - # Setting created_at time only allowed for admins and project owners - unless current_user.admin? || user_project.owner == current_user - params.delete(:created_at) - end - - issue_params = declared_params(include_missing: false) - issue_params = issue_params.merge(merge_request_to_resolve_discussions_of: issue_params.delete(:merge_request_for_resolving_discussions)) - issue_params = convert_parameters_from_legacy_format(issue_params) - - issue = ::Issues::CreateService.new(user_project, - current_user, - issue_params.merge(request: request, api: true)).execute - render_spam_error! if issue.spam? - - if issue.valid? - present issue, with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project - else - render_validation_error!(issue) - end - end - - desc 'Update an existing issue' do - success ::API::V3::Entities::Issue - end - params do - requires :issue_id, type: Integer, desc: 'The ID of a project issue' - optional :title, type: String, desc: 'The title of an issue' - optional :updated_at, type: DateTime, - desc: 'Date time when the issue was updated. Available only for admins and project owners.' - optional :state_event, type: String, values: %w[reopen close], desc: 'State of the issue' - use :issue_params - at_least_one_of :title, :description, :assignee_id, :milestone_id, - :labels, :created_at, :due_date, :confidential, :state_event - end - put ':id/issues/:issue_id' do - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42132') - - issue = user_project.issues.find(params.delete(:issue_id)) - authorize! :update_issue, issue - - # Setting created_at time only allowed for admins and project owners - unless current_user.admin? || user_project.owner == current_user - params.delete(:updated_at) - end - - update_params = declared_params(include_missing: false).merge(request: request, api: true) - update_params = convert_parameters_from_legacy_format(update_params) - - issue = ::Issues::UpdateService.new(user_project, - current_user, - update_params).execute(issue) - - render_spam_error! if issue.spam? - - if issue.valid? - present issue, with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project - else - render_validation_error!(issue) - end - end - - desc 'Move an existing issue' do - success ::API::V3::Entities::Issue - end - params do - requires :issue_id, type: Integer, desc: 'The ID of a project issue' - requires :to_project_id, type: Integer, desc: 'The ID of the new project' - end - post ':id/issues/:issue_id/move' do - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42133') - - issue = user_project.issues.find_by(id: params[:issue_id]) - not_found!('Issue') unless issue - - new_project = Project.find_by(id: params[:to_project_id]) - not_found!('Project') unless new_project - - begin - issue = ::Issues::MoveService.new(user_project, current_user).execute(issue, new_project) - present issue, with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project - rescue ::Issues::MoveService::MoveError => error - render_api_error!(error.message, 400) - end - end - - desc 'Delete a project issue' - params do - requires :issue_id, type: Integer, desc: 'The ID of a project issue' - end - delete ":id/issues/:issue_id" do - issue = user_project.issues.find_by(id: params[:issue_id]) - not_found!('Issue') unless issue - - authorize!(:destroy_issue, issue) - - status(200) - issue.destroy - end - end - end - end -end diff --git a/lib/api/v3/labels.rb b/lib/api/v3/labels.rb deleted file mode 100644 index 4157462ec2a..00000000000 --- a/lib/api/v3/labels.rb +++ /dev/null @@ -1,34 +0,0 @@ -module API - module V3 - class Labels < Grape::API - before { authenticate! } - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Get all labels of the project' do - success ::API::Entities::Label - end - get ':id/labels' do - present available_labels_for(user_project), with: ::API::Entities::Label, current_user: current_user, project: user_project - end - - desc 'Delete an existing label' do - success ::API::Entities::Label - end - params do - requires :name, type: String, desc: 'The name of the label to be deleted' - end - delete ':id/labels' do - authorize! :admin_label, user_project - - label = user_project.labels.find_by(title: params[:name]) - not_found!('Label') unless label - - present label.destroy, with: ::API::Entities::Label, current_user: current_user, project: user_project - end - end - end - end -end diff --git a/lib/api/v3/members.rb b/lib/api/v3/members.rb deleted file mode 100644 index 88dd598f1e9..00000000000 --- a/lib/api/v3/members.rb +++ /dev/null @@ -1,136 +0,0 @@ -module API - module V3 - class Members < Grape::API - include PaginationParams - - before { authenticate! } - - helpers ::API::Helpers::MembersHelpers - - %w[group project].each do |source_type| - params do - requires :id, type: String, desc: "The #{source_type} ID" - end - resource source_type.pluralize, requirements: { id: %r{[^/]+} } do - desc 'Gets a list of group or project members viewable by the authenticated user.' do - success ::API::Entities::Member - end - params do - optional :query, type: String, desc: 'A query string to search for members' - use :pagination - end - get ":id/members" do - source = find_source(source_type, params[:id]) - - members = source.members.where.not(user_id: nil).includes(:user) - members = members.joins(:user).merge(User.search(params[:query])) if params[:query].present? - members = paginate(members) - - present members, with: ::API::Entities::Member - end - - desc 'Gets a member of a group or project.' do - success ::API::Entities::Member - end - params do - requires :user_id, type: Integer, desc: 'The user ID of the member' - end - get ":id/members/:user_id" do - source = find_source(source_type, params[:id]) - - members = source.members - member = members.find_by!(user_id: params[:user_id]) - - present member, with: ::API::Entities::Member - end - - desc 'Adds a member to a group or project.' do - success ::API::Entities::Member - end - params do - requires :user_id, type: Integer, desc: 'The user ID of the new member' - requires :access_level, type: Integer, desc: 'A valid access level (defaults: `30`, developer access level)' - optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY' - end - post ":id/members" do - source = find_source(source_type, params[:id]) - authorize_admin_source!(source_type, source) - - member = source.members.find_by(user_id: params[:user_id]) - - # We need this explicit check because `source.add_user` doesn't - # currently return the member created so it would return 201 even if - # the member already existed... - # The `source_type == 'group'` check is to ensure back-compatibility - # but 409 behavior should be used for both project and group members in 9.0! - conflict!('Member already exists') if source_type == 'group' && member - - unless member - member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at]) - end - - if member.persisted? && member.valid? - present member, with: ::API::Entities::Member - else - # This is to ensure back-compatibility but 400 behavior should be used - # for all validation errors in 9.0! - render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level) - render_validation_error!(member) - end - end - - desc 'Updates a member of a group or project.' do - success ::API::Entities::Member - end - params do - requires :user_id, type: Integer, desc: 'The user ID of the new member' - requires :access_level, type: Integer, desc: 'A valid access level' - optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY' - end - put ":id/members/:user_id" do - source = find_source(source_type, params.delete(:id)) - authorize_admin_source!(source_type, source) - - member = source.members.find_by!(user_id: params.delete(:user_id)) - - if member.update_attributes(declared_params(include_missing: false)) - present member, with: ::API::Entities::Member - else - # This is to ensure back-compatibility but 400 behavior should be used - # for all validation errors in 9.0! - render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level) - render_validation_error!(member) - end - end - - desc 'Removes a user from a group or project.' - params do - requires :user_id, type: Integer, desc: 'The user ID of the member' - end - delete ":id/members/:user_id" do - source = find_source(source_type, params[:id]) - - # This is to ensure back-compatibility but find_by! should be used - # in that casse in 9.0! - member = source.members.find_by(user_id: params[:user_id]) - - # This is to ensure back-compatibility but this should be removed in - # favor of find_by! in 9.0! - not_found!("Member: user_id:#{params[:user_id]}") if source_type == 'group' && member.nil? - - # This is to ensure back-compatibility but 204 behavior should be used - # for all DELETE endpoints in 9.0! - if member.nil? - status(200 ) - { message: "Access revoked", id: params[:user_id].to_i } - else - ::Members::DestroyService.new(current_user).execute(member) - - present member, with: ::API::Entities::Member - end - end - end - end - end - end -end diff --git a/lib/api/v3/merge_request_diffs.rb b/lib/api/v3/merge_request_diffs.rb deleted file mode 100644 index 22866fc2845..00000000000 --- a/lib/api/v3/merge_request_diffs.rb +++ /dev/null @@ -1,44 +0,0 @@ -module API - module V3 - # MergeRequestDiff API - class MergeRequestDiffs < Grape::API - before { authenticate! } - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Get a list of merge request diff versions' do - detail 'This feature was introduced in GitLab 8.12.' - success ::API::Entities::MergeRequestDiff - end - - params do - requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' - end - - get ":id/merge_requests/:merge_request_id/versions" do - merge_request = find_merge_request_with_access(params[:merge_request_id]) - - present merge_request.merge_request_diffs.order_id_desc, with: ::API::Entities::MergeRequestDiff - end - - desc 'Get a single merge request diff version' do - detail 'This feature was introduced in GitLab 8.12.' - success ::API::Entities::MergeRequestDiffFull - end - - params do - requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' - requires :version_id, type: Integer, desc: 'The ID of a merge request diff version' - end - - get ":id/merge_requests/:merge_request_id/versions/:version_id" do - merge_request = find_merge_request_with_access(params[:merge_request_id]) - - present merge_request.merge_request_diffs.find(params[:version_id]), with: ::API::Entities::MergeRequestDiffFull - end - end - end - end -end diff --git a/lib/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb deleted file mode 100644 index 9b0f70e2bfe..00000000000 --- a/lib/api/v3/merge_requests.rb +++ /dev/null @@ -1,297 +0,0 @@ -module API - module V3 - class MergeRequests < Grape::API - include PaginationParams - - DEPRECATION_MESSAGE = 'This endpoint is deprecated and has been removed on V4'.freeze - - before { authenticate! } - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - include TimeTrackingEndpoints - - helpers do - def handle_merge_request_errors!(errors) - if errors[:project_access].any? - error!(errors[:project_access], 422) - elsif errors[:branch_conflict].any? - error!(errors[:branch_conflict], 422) - elsif errors[:validate_fork].any? - error!(errors[:validate_fork], 422) - elsif errors[:validate_branches].any? - conflict!(errors[:validate_branches]) - elsif errors[:base].any? - error!(errors[:base], 422) - end - - render_api_error!(errors, 400) - end - - def issue_entity(project) - if project.has_external_issue_tracker? - ::API::Entities::ExternalIssue - else - ::API::V3::Entities::Issue - end - end - - params :optional_params do - optional :description, type: String, desc: 'The description of the merge request' - optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request' - optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request' - optional :labels, type: String, desc: 'Comma-separated list of label names' - optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging' - end - end - - desc 'List merge requests' do - detail 'iid filter is deprecated have been removed on V4' - success ::API::V3::Entities::MergeRequest - end - params do - optional :state, type: String, values: %w[opened closed merged all], default: 'all', - desc: 'Return opened, closed, merged, or all merge requests' - optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at', - desc: 'Return merge requests ordered by `created_at` or `updated_at` fields.' - optional :sort, type: String, values: %w[asc desc], default: 'desc', - desc: 'Return merge requests sorted in `asc` or `desc` order.' - optional :iid, type: Array[Integer], desc: 'The IID of the merge requests' - use :pagination - end - get ":id/merge_requests" do - authorize! :read_merge_request, user_project - - merge_requests = user_project.merge_requests.inc_notes_with_associations - merge_requests = filter_by_iid(merge_requests, params[:iid]) if params[:iid].present? - - merge_requests = - case params[:state] - when 'opened' then merge_requests.opened - when 'closed' then merge_requests.closed - when 'merged' then merge_requests.merged - else merge_requests - end - - merge_requests = merge_requests.reorder(params[:order_by] => params[:sort]) - present paginate(merge_requests), with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project - end - - desc 'Create a merge request' do - success ::API::V3::Entities::MergeRequest - end - params do - requires :title, type: String, desc: 'The title of the merge request' - requires :source_branch, type: String, desc: 'The source branch' - requires :target_branch, type: String, desc: 'The target branch' - optional :target_project_id, type: Integer, - desc: 'The target project of the merge request defaults to the :id of the project' - use :optional_params - end - post ":id/merge_requests" do - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42126') - - authorize! :create_merge_request_from, user_project - - mr_params = declared_params(include_missing: false) - mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present? - - merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute - - if merge_request.valid? - present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project - else - handle_merge_request_errors! merge_request.errors - end - end - - desc 'Delete a merge request' - params do - requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' - end - delete ":id/merge_requests/:merge_request_id" do - merge_request = find_project_merge_request(params[:merge_request_id]) - - authorize!(:destroy_merge_request, merge_request) - - status(200) - merge_request.destroy - end - - params do - requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' - end - { ":id/merge_request/:merge_request_id" => :deprecated, ":id/merge_requests/:merge_request_id" => :ok }.each do |path, status| - desc 'Get a single merge request' do - if status == :deprecated - detail DEPRECATION_MESSAGE - end - - success ::API::V3::Entities::MergeRequest - end - get path do - merge_request = find_merge_request_with_access(params[:merge_request_id]) - - present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project - end - - desc 'Get the commits of a merge request' do - success ::API::Entities::Commit - end - get "#{path}/commits" do - merge_request = find_merge_request_with_access(params[:merge_request_id]) - - present merge_request.commits, with: ::API::Entities::Commit - end - - desc 'Show the merge request changes' do - success ::API::Entities::MergeRequestChanges - end - get "#{path}/changes" do - merge_request = find_merge_request_with_access(params[:merge_request_id]) - - present merge_request, with: ::API::Entities::MergeRequestChanges, current_user: current_user - end - - desc 'Update a merge request' do - success ::API::V3::Entities::MergeRequest - end - params do - optional :title, type: String, allow_blank: false, desc: 'The title of the merge request' - optional :target_branch, type: String, allow_blank: false, desc: 'The target branch' - optional :state_event, type: String, values: %w[close reopen merge], - desc: 'Status of the merge request' - use :optional_params - at_least_one_of :title, :target_branch, :description, :assignee_id, - :milestone_id, :labels, :state_event, - :remove_source_branch - end - put path do - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42127') - - merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :update_merge_request) - - mr_params = declared_params(include_missing: false) - mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present? - - merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request) - - if merge_request.valid? - present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project - else - handle_merge_request_errors! merge_request.errors - end - end - - desc 'Merge a merge request' do - success ::API::V3::Entities::MergeRequest - end - params do - optional :merge_commit_message, type: String, desc: 'Custom merge commit message' - optional :should_remove_source_branch, type: Boolean, - desc: 'When true, the source branch will be deleted if possible' - optional :merge_when_build_succeeds, type: Boolean, - desc: 'When true, this merge request will be merged when the build succeeds' - optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch' - end - put "#{path}/merge" do - merge_request = find_project_merge_request(params[:merge_request_id]) - - # Merge request can not be merged - # because user dont have permissions to push into target branch - unauthorized! unless merge_request.can_be_merged_by?(current_user) - - not_allowed! unless merge_request.mergeable_state? - - render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable? - - if params[:sha] && merge_request.diff_head_sha != params[:sha] - render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409) - end - - merge_params = { - commit_message: params[:merge_commit_message], - should_remove_source_branch: params[:should_remove_source_branch] - } - - if params[:merge_when_build_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active? - ::MergeRequests::MergeWhenPipelineSucceedsService - .new(merge_request.target_project, current_user, merge_params) - .execute(merge_request) - else - ::MergeRequests::MergeService - .new(merge_request.target_project, current_user, merge_params) - .execute(merge_request) - end - - present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project - end - - desc 'Cancel merge if "Merge When Build succeeds" is enabled' do - success ::API::V3::Entities::MergeRequest - end - post "#{path}/cancel_merge_when_build_succeeds" do - merge_request = find_project_merge_request(params[:merge_request_id]) - - unauthorized! unless merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user) - - ::MergeRequest::MergeWhenPipelineSucceedsService - .new(merge_request.target_project, current_user) - .cancel(merge_request) - end - - desc 'Get the comments of a merge request' do - detail 'Duplicate. DEPRECATED and HAS BEEN REMOVED in V4' - success ::API::Entities::MRNote - end - params do - use :pagination - end - get "#{path}/comments" do - merge_request = find_merge_request_with_access(params[:merge_request_id]) - present paginate(merge_request.notes.fresh), with: ::API::Entities::MRNote - end - - desc 'Post a comment to a merge request' do - detail 'Duplicate. DEPRECATED and HAS BEEN REMOVED in V4' - success ::API::Entities::MRNote - end - params do - requires :note, type: String, desc: 'The text of the comment' - end - post "#{path}/comments" do - merge_request = find_merge_request_with_access(params[:merge_request_id], :create_note) - - opts = { - note: params[:note], - noteable_type: 'MergeRequest', - noteable_id: merge_request.id - } - - note = ::Notes::CreateService.new(user_project, current_user, opts).execute - - if note.save - present note, with: ::API::Entities::MRNote - else - render_api_error!("Failed to save note #{note.errors.messages}", 400) - end - end - - desc 'List issues that will be closed on merge' do - success ::API::Entities::MRNote - end - params do - use :pagination - end - get "#{path}/closes_issues" do - merge_request = find_merge_request_with_access(params[:merge_request_id]) - issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user)) - present paginate(issues), with: issue_entity(user_project), current_user: current_user - end - end - end - end - end -end diff --git a/lib/api/v3/milestones.rb b/lib/api/v3/milestones.rb deleted file mode 100644 index 9be4cf9d22a..00000000000 --- a/lib/api/v3/milestones.rb +++ /dev/null @@ -1,65 +0,0 @@ -module API - module V3 - class Milestones < Grape::API - include PaginationParams - - before { authenticate! } - - helpers do - def filter_milestones_state(milestones, state) - case state - when 'active' then milestones.active - when 'closed' then milestones.closed - else milestones - end - end - end - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Get a list of project milestones' do - success ::API::Entities::Milestone - end - params do - optional :state, type: String, values: %w[active closed all], default: 'all', - desc: 'Return "active", "closed", or "all" milestones' - optional :iid, type: Array[Integer], desc: 'The IID of the milestone' - use :pagination - end - get ":id/milestones" do - authorize! :read_milestone, user_project - - milestones = user_project.milestones - milestones = filter_milestones_state(milestones, params[:state]) - milestones = filter_by_iid(milestones, params[:iid]) if params[:iid].present? - milestones = milestones.order_id_desc - - present paginate(milestones), with: ::API::Entities::Milestone - end - - desc 'Get all issues for a single project milestone' do - success ::API::V3::Entities::Issue - end - params do - requires :milestone_id, type: Integer, desc: 'The ID of a project milestone' - use :pagination - end - get ':id/milestones/:milestone_id/issues' do - authorize! :read_milestone, user_project - - milestone = user_project.milestones.find(params[:milestone_id]) - - finder_params = { - project_id: user_project.id, - milestone_title: milestone.title - } - - issues = IssuesFinder.new(current_user, finder_params).execute - present paginate(issues), with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project - end - end - end - end -end diff --git a/lib/api/v3/notes.rb b/lib/api/v3/notes.rb deleted file mode 100644 index d49772b92f2..00000000000 --- a/lib/api/v3/notes.rb +++ /dev/null @@ -1,148 +0,0 @@ -module API - module V3 - class Notes < Grape::API - include PaginationParams - - before { authenticate! } - - NOTEABLE_TYPES = [Issue, MergeRequest, Snippet].freeze - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - NOTEABLE_TYPES.each do |noteable_type| - noteables_str = noteable_type.to_s.underscore.pluralize - - desc 'Get a list of project +noteable+ notes' do - success ::API::V3::Entities::Note - end - params do - requires :noteable_id, type: Integer, desc: 'The ID of the noteable' - use :pagination - end - get ":id/#{noteables_str}/:noteable_id/notes" do - noteable = user_project.public_send(noteables_str.to_sym).find(params[:noteable_id]) # rubocop:disable GitlabSecurity/PublicSend - - if can?(current_user, noteable_read_ability_name(noteable), noteable) - # We exclude notes that are cross-references and that cannot be viewed - # by the current user. By doing this exclusion at this level and not - # at the DB query level (which we cannot in that case), the current - # page can have less elements than :per_page even if - # there's more than one page. - notes = - # paginate() only works with a relation. This could lead to a - # mismatch between the pagination headers info and the actual notes - # array returned, but this is really a edge-case. - paginate(noteable.notes) - .reject { |n| n.cross_reference_not_visible_for?(current_user) } - present notes, with: ::API::V3::Entities::Note - else - not_found!("Notes") - end - end - - desc 'Get a single +noteable+ note' do - success ::API::V3::Entities::Note - end - params do - requires :note_id, type: Integer, desc: 'The ID of a note' - requires :noteable_id, type: Integer, desc: 'The ID of the noteable' - end - get ":id/#{noteables_str}/:noteable_id/notes/:note_id" do - noteable = user_project.public_send(noteables_str.to_sym).find(params[:noteable_id]) # rubocop:disable GitlabSecurity/PublicSend - note = noteable.notes.find(params[:note_id]) - can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user) - - if can_read_note - present note, with: ::API::V3::Entities::Note - else - not_found!("Note") - end - end - - desc 'Create a new +noteable+ note' do - success ::API::V3::Entities::Note - end - params do - requires :noteable_id, type: Integer, desc: 'The ID of the noteable' - requires :body, type: String, desc: 'The content of a note' - optional :created_at, type: String, desc: 'The creation date of the note' - end - post ":id/#{noteables_str}/:noteable_id/notes" do - opts = { - note: params[:body], - noteable_type: noteables_str.classify, - noteable_id: params[:noteable_id] - } - - noteable = user_project.public_send(noteables_str.to_sym).find(params[:noteable_id]) # rubocop:disable GitlabSecurity/PublicSend - - if can?(current_user, noteable_read_ability_name(noteable), noteable) - if params[:created_at] && (current_user.admin? || user_project.owner == current_user) - opts[:created_at] = params[:created_at] - end - - note = ::Notes::CreateService.new(user_project, current_user, opts).execute - if note.valid? - present note, with: ::API::V3::Entities.const_get(note.class.name) - else - not_found!("Note #{note.errors.messages}") - end - else - not_found!("Note") - end - end - - desc 'Update an existing +noteable+ note' do - success ::API::V3::Entities::Note - end - params do - requires :noteable_id, type: Integer, desc: 'The ID of the noteable' - requires :note_id, type: Integer, desc: 'The ID of a note' - requires :body, type: String, desc: 'The content of a note' - end - put ":id/#{noteables_str}/:noteable_id/notes/:note_id" do - note = user_project.notes.find(params[:note_id]) - - authorize! :admin_note, note - - opts = { - note: params[:body] - } - - note = ::Notes::UpdateService.new(user_project, current_user, opts).execute(note) - - if note.valid? - present note, with: ::API::V3::Entities::Note - else - render_api_error!("Failed to save note #{note.errors.messages}", 400) - end - end - - desc 'Delete a +noteable+ note' do - success ::API::V3::Entities::Note - end - params do - requires :noteable_id, type: Integer, desc: 'The ID of the noteable' - requires :note_id, type: Integer, desc: 'The ID of a note' - end - delete ":id/#{noteables_str}/:noteable_id/notes/:note_id" do - note = user_project.notes.find(params[:note_id]) - authorize! :admin_note, note - - ::Notes::DestroyService.new(user_project, current_user).execute(note) - - present note, with: ::API::V3::Entities::Note - end - end - end - - helpers do - def noteable_read_ability_name(noteable) - "read_#{noteable.class.to_s.underscore}".to_sym - end - end - end - end -end diff --git a/lib/api/v3/pipelines.rb b/lib/api/v3/pipelines.rb deleted file mode 100644 index 6d31c12f572..00000000000 --- a/lib/api/v3/pipelines.rb +++ /dev/null @@ -1,38 +0,0 @@ -module API - module V3 - class Pipelines < Grape::API - include PaginationParams - - before { authenticate! } - - params do - requires :id, type: String, desc: 'The project ID' - end - resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Get all Pipelines of the project' do - detail 'This feature was introduced in GitLab 8.11.' - success ::API::Entities::Pipeline - end - params do - use :pagination - optional :scope, type: String, values: %w(running branches tags), - desc: 'Either running, branches, or tags' - end - get ':id/pipelines' do - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42123') - - authorize! :read_pipeline, user_project - - pipelines = PipelinesFinder.new(user_project, scope: params[:scope]).execute - present paginate(pipelines), with: ::API::Entities::Pipeline - end - end - - helpers do - def pipeline - @pipeline ||= user_project.pipelines.find(params[:pipeline_id]) - end - end - end - end -end diff --git a/lib/api/v3/project_hooks.rb b/lib/api/v3/project_hooks.rb deleted file mode 100644 index 631944150c7..00000000000 --- a/lib/api/v3/project_hooks.rb +++ /dev/null @@ -1,111 +0,0 @@ -module API - module V3 - class ProjectHooks < Grape::API - include PaginationParams - - before { authenticate! } - before { authorize_admin_project } - - helpers do - params :project_hook_properties do - requires :url, type: String, desc: "The URL to send the request to" - optional :push_events, type: Boolean, desc: "Trigger hook on push events" - optional :issues_events, type: Boolean, desc: "Trigger hook on issues events" - optional :confidential_issues_events, type: Boolean, desc: "Trigger hook on confidential issues events" - optional :merge_requests_events, type: Boolean, desc: "Trigger hook on merge request events" - optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events" - optional :note_events, type: Boolean, desc: "Trigger hook on note(comment) events" - optional :build_events, type: Boolean, desc: "Trigger hook on build events" - optional :pipeline_events, type: Boolean, desc: "Trigger hook on pipeline events" - optional :wiki_page_events, type: Boolean, desc: "Trigger hook on wiki events" - optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook" - optional :token, type: String, desc: "Secret token to validate received payloads; this will not be returned in the response" - end - end - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Get project hooks' do - success ::API::V3::Entities::ProjectHook - end - params do - use :pagination - end - get ":id/hooks" do - hooks = paginate user_project.hooks - - present hooks, with: ::API::V3::Entities::ProjectHook - end - - desc 'Get a project hook' do - success ::API::V3::Entities::ProjectHook - end - params do - requires :hook_id, type: Integer, desc: 'The ID of a project hook' - end - get ":id/hooks/:hook_id" do - hook = user_project.hooks.find(params[:hook_id]) - present hook, with: ::API::V3::Entities::ProjectHook - end - - desc 'Add hook to project' do - success ::API::V3::Entities::ProjectHook - end - params do - use :project_hook_properties - end - post ":id/hooks" do - attrs = declared_params(include_missing: false) - attrs[:job_events] = attrs.delete(:build_events) if attrs.key?(:build_events) - hook = user_project.hooks.new(attrs) - - if hook.save - present hook, with: ::API::V3::Entities::ProjectHook - else - error!("Invalid url given", 422) if hook.errors[:url].present? - - not_found!("Project hook #{hook.errors.messages}") - end - end - - desc 'Update an existing project hook' do - success ::API::V3::Entities::ProjectHook - end - params do - requires :hook_id, type: Integer, desc: "The ID of the hook to update" - use :project_hook_properties - end - put ":id/hooks/:hook_id" do - hook = user_project.hooks.find(params.delete(:hook_id)) - - attrs = declared_params(include_missing: false) - attrs[:job_events] = attrs.delete(:build_events) if attrs.key?(:build_events) - if hook.update_attributes(attrs) - present hook, with: ::API::V3::Entities::ProjectHook - else - error!("Invalid url given", 422) if hook.errors[:url].present? - - not_found!("Project hook #{hook.errors.messages}") - end - end - - desc 'Deletes project hook' do - success ::API::V3::Entities::ProjectHook - end - params do - requires :hook_id, type: Integer, desc: 'The ID of the hook to delete' - end - delete ":id/hooks/:hook_id" do - begin - present user_project.hooks.destroy(params[:hook_id]), with: ::API::V3::Entities::ProjectHook - rescue - # ProjectHook can raise Error if hook_id not found - not_found!("Error deleting hook #{params[:hook_id]}") - end - end - end - end - end -end diff --git a/lib/api/v3/project_snippets.rb b/lib/api/v3/project_snippets.rb deleted file mode 100644 index 6ba425ba8c7..00000000000 --- a/lib/api/v3/project_snippets.rb +++ /dev/null @@ -1,143 +0,0 @@ -module API - module V3 - class ProjectSnippets < Grape::API - include PaginationParams - - before { authenticate! } - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - helpers do - def handle_project_member_errors(errors) - if errors[:project_access].any? - error!(errors[:project_access], 422) - end - - not_found! - end - - def snippets_for_current_user - SnippetsFinder.new(current_user, project: user_project).execute - end - end - - desc 'Get all project snippets' do - success ::API::V3::Entities::ProjectSnippet - end - params do - use :pagination - end - get ":id/snippets" do - present paginate(snippets_for_current_user), with: ::API::V3::Entities::ProjectSnippet - end - - desc 'Get a single project snippet' do - success ::API::V3::Entities::ProjectSnippet - end - params do - requires :snippet_id, type: Integer, desc: 'The ID of a project snippet' - end - get ":id/snippets/:snippet_id" do - snippet = snippets_for_current_user.find(params[:snippet_id]) - present snippet, with: ::API::V3::Entities::ProjectSnippet - end - - desc 'Create a new project snippet' do - success ::API::V3::Entities::ProjectSnippet - end - params do - requires :title, type: String, desc: 'The title of the snippet' - requires :file_name, type: String, desc: 'The file name of the snippet' - requires :code, type: String, desc: 'The content of the snippet' - requires :visibility_level, type: Integer, - values: [Gitlab::VisibilityLevel::PRIVATE, - Gitlab::VisibilityLevel::INTERNAL, - Gitlab::VisibilityLevel::PUBLIC], - desc: 'The visibility level of the snippet' - end - post ":id/snippets" do - authorize! :create_project_snippet, user_project - snippet_params = declared_params.merge(request: request, api: true) - snippet_params[:content] = snippet_params.delete(:code) - - snippet = CreateSnippetService.new(user_project, current_user, snippet_params).execute - - render_spam_error! if snippet.spam? - - if snippet.persisted? - present snippet, with: ::API::V3::Entities::ProjectSnippet - else - render_validation_error!(snippet) - end - end - - desc 'Update an existing project snippet' do - success ::API::V3::Entities::ProjectSnippet - end - params do - requires :snippet_id, type: Integer, desc: 'The ID of a project snippet' - optional :title, type: String, desc: 'The title of the snippet' - optional :file_name, type: String, desc: 'The file name of the snippet' - optional :code, type: String, desc: 'The content of the snippet' - optional :visibility_level, type: Integer, - values: [Gitlab::VisibilityLevel::PRIVATE, - Gitlab::VisibilityLevel::INTERNAL, - Gitlab::VisibilityLevel::PUBLIC], - desc: 'The visibility level of the snippet' - at_least_one_of :title, :file_name, :code, :visibility_level - end - put ":id/snippets/:snippet_id" do - snippet = snippets_for_current_user.find_by(id: params.delete(:snippet_id)) - not_found!('Snippet') unless snippet - - authorize! :update_project_snippet, snippet - - snippet_params = declared_params(include_missing: false) - .merge(request: request, api: true) - - snippet_params[:content] = snippet_params.delete(:code) if snippet_params[:code].present? - - UpdateSnippetService.new(user_project, current_user, snippet, - snippet_params).execute - - render_spam_error! if snippet.spam? - - if snippet.valid? - present snippet, with: ::API::V3::Entities::ProjectSnippet - else - render_validation_error!(snippet) - end - end - - desc 'Delete a project snippet' - params do - requires :snippet_id, type: Integer, desc: 'The ID of a project snippet' - end - delete ":id/snippets/:snippet_id" do - snippet = snippets_for_current_user.find_by(id: params[:snippet_id]) - not_found!('Snippet') unless snippet - - authorize! :admin_project_snippet, snippet - snippet.destroy - - status(200) - end - - desc 'Get a raw project snippet' - params do - requires :snippet_id, type: Integer, desc: 'The ID of a project snippet' - end - get ":id/snippets/:snippet_id/raw" do - snippet = snippets_for_current_user.find_by(id: params[:snippet_id]) - not_found!('Snippet') unless snippet - - env['api.format'] = :txt - content_type 'text/plain' - present snippet.content - end - end - end - end -end diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb deleted file mode 100644 index eb3dd113524..00000000000 --- a/lib/api/v3/projects.rb +++ /dev/null @@ -1,475 +0,0 @@ -module API - module V3 - class Projects < Grape::API - include PaginationParams - - before { authenticate_non_get! } - - after_validation do - set_only_allow_merge_if_pipeline_succeeds! - end - - helpers do - params :optional_params do - optional :description, type: String, desc: 'The description of the project' - optional :issues_enabled, type: Boolean, desc: 'Flag indication if the issue tracker is enabled' - optional :merge_requests_enabled, type: Boolean, desc: 'Flag indication if merge requests are enabled' - optional :wiki_enabled, type: Boolean, desc: 'Flag indication if the wiki is enabled' - optional :builds_enabled, type: Boolean, desc: 'Flag indication if builds are enabled' - optional :snippets_enabled, type: Boolean, desc: 'Flag indication if snippets are enabled' - optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project' - optional :resolve_outdated_diff_discussions, type: Boolean, desc: 'Automatically resolve merge request diffs discussions on lines changed with a push' - optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project' - optional :lfs_enabled, type: Boolean, desc: 'Flag indication if Git LFS is enabled for that project' - optional :public, type: Boolean, desc: 'Create a public project. The same as visibility_level = 20.' - optional :visibility_level, type: Integer, values: [ - Gitlab::VisibilityLevel::PRIVATE, - Gitlab::VisibilityLevel::INTERNAL, - Gitlab::VisibilityLevel::PUBLIC - ], desc: 'Create a public project. The same as visibility_level = 20.' - optional :public_builds, type: Boolean, desc: 'Perform public builds' - optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access' - optional :only_allow_merge_if_build_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed' - optional :only_allow_merge_if_pipeline_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed' - optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved' - end - - def map_public_to_visibility_level(attrs) - publik = attrs.delete(:public) - if !publik.nil? && !attrs[:visibility_level].present? - # Since setting the public attribute to private could mean either - # private or internal, use the more conservative option, private. - attrs[:visibility_level] = (publik == true) ? Gitlab::VisibilityLevel::PUBLIC : Gitlab::VisibilityLevel::PRIVATE - end - - attrs - end - - def set_only_allow_merge_if_pipeline_succeeds! - if params.key?(:only_allow_merge_if_build_succeeds) - params[:only_allow_merge_if_pipeline_succeeds] = params.delete(:only_allow_merge_if_build_succeeds) - end - end - end - - resource :projects do - helpers do - params :collection_params do - use :sort_params - use :filter_params - use :pagination - - optional :simple, type: Boolean, default: false, - desc: 'Return only the ID, URL, name, and path of each project' - end - - params :sort_params do - optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at], - default: 'created_at', desc: 'Return projects ordered by field' - optional :sort, type: String, values: %w[asc desc], default: 'desc', - desc: 'Return projects sorted in ascending and descending order' - end - - params :filter_params do - optional :archived, type: Boolean, default: nil, desc: 'Limit by archived status' - optional :visibility, type: String, values: %w[public internal private], - desc: 'Limit by visibility' - optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria' - end - - params :statistics_params do - optional :statistics, type: Boolean, default: false, desc: 'Include project statistics' - end - - params :create_params do - optional :namespace_id, type: Integer, desc: 'Namespace ID for the new project. Default to the user namespace.' - optional :import_url, type: String, desc: 'URL from which the project is imported' - end - - def present_projects(projects, options = {}) - options = options.reverse_merge( - with: ::API::V3::Entities::Project, - current_user: current_user, - simple: params[:simple] - ) - - projects = filter_projects(projects) - projects = projects.with_statistics if options[:statistics] - options[:with] = ::API::Entities::BasicProjectDetails if options[:simple] - - present paginate(projects), options - end - end - - desc 'Get a list of visible projects for authenticated user' do - success ::API::Entities::BasicProjectDetails - end - params do - use :collection_params - end - get '/visible' do - entity = current_user ? ::API::V3::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails - present_projects ProjectsFinder.new(current_user: current_user).execute, with: entity - end - - desc 'Get a projects list for authenticated user' do - success ::API::Entities::BasicProjectDetails - end - params do - use :collection_params - end - get do - authenticate! - - present_projects current_user.authorized_projects.order_id_desc, - with: ::API::V3::Entities::ProjectWithAccess - end - - desc 'Get an owned projects list for authenticated user' do - success ::API::Entities::BasicProjectDetails - end - params do - use :collection_params - use :statistics_params - end - get '/owned' do - authenticate! - - present_projects current_user.owned_projects, - with: ::API::V3::Entities::ProjectWithAccess, - statistics: params[:statistics] - end - - desc 'Gets starred project for the authenticated user' do - success ::API::Entities::BasicProjectDetails - end - params do - use :collection_params - end - get '/starred' do - authenticate! - - present_projects ProjectsFinder.new(current_user: current_user, params: { starred: true }).execute - end - - desc 'Get all projects for admin user' do - success ::API::Entities::BasicProjectDetails - end - params do - use :collection_params - use :statistics_params - end - get '/all' do - authenticated_as_admin! - - present_projects Project.all, with: ::API::V3::Entities::ProjectWithAccess, statistics: params[:statistics] - end - - desc 'Search for projects the current user has access to' do - success ::API::V3::Entities::Project - end - params do - requires :query, type: String, desc: 'The project name to be searched' - use :sort_params - use :pagination - end - get "/search/:query", requirements: { query: %r{[^/]+} } do - search_service = ::Search::GlobalService.new(current_user, search: params[:query]).execute - projects = search_service.objects('projects', params[:page], false) - projects = projects.reorder(params[:order_by] => params[:sort]) - - present paginate(projects), with: ::API::V3::Entities::Project - end - - desc 'Create new project' do - success ::API::V3::Entities::Project - end - params do - optional :name, type: String, desc: 'The name of the project' - optional :path, type: String, desc: 'The path of the repository' - at_least_one_of :name, :path - use :optional_params - use :create_params - end - post do - attrs = map_public_to_visibility_level(declared_params(include_missing: false)) - project = ::Projects::CreateService.new(current_user, attrs).execute - - if project.saved? - present project, with: ::API::V3::Entities::Project, - user_can_admin_project: can?(current_user, :admin_project, project) - else - if project.errors[:limit_reached].present? - error!(project.errors[:limit_reached], 403) - end - - render_validation_error!(project) - end - end - - desc 'Create new project for a specified user. Only available to admin users.' do - success ::API::V3::Entities::Project - end - params do - requires :name, type: String, desc: 'The name of the project' - requires :user_id, type: Integer, desc: 'The ID of a user' - optional :default_branch, type: String, desc: 'The default branch of the project' - use :optional_params - use :create_params - end - post "user/:user_id" do - authenticated_as_admin! - user = User.find_by(id: params.delete(:user_id)) - not_found!('User') unless user - - attrs = map_public_to_visibility_level(declared_params(include_missing: false)) - project = ::Projects::CreateService.new(user, attrs).execute - - if project.saved? - present project, with: ::API::V3::Entities::Project, - user_can_admin_project: can?(current_user, :admin_project, project) - else - render_validation_error!(project) - end - end - end - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Get a single project' do - success ::API::V3::Entities::ProjectWithAccess - end - get ":id" do - entity = current_user ? ::API::V3::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails - present user_project, with: entity, current_user: current_user, - user_can_admin_project: can?(current_user, :admin_project, user_project) - end - - desc 'Get events for a single project' do - success ::API::V3::Entities::Event - end - params do - use :pagination - end - get ":id/events" do - present paginate(user_project.events.recent), with: ::API::V3::Entities::Event - end - - desc 'Fork new project for the current user or provided namespace.' do - success ::API::V3::Entities::Project - end - params do - optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be forked into' - end - post 'fork/:id' do - fork_params = declared_params(include_missing: false) - namespace_id = fork_params[:namespace] - - if namespace_id.present? - fork_params[:namespace] = find_namespace(namespace_id) - - unless fork_params[:namespace] && can?(current_user, :create_projects, fork_params[:namespace]) - not_found!('Target Namespace') - end - end - - forked_project = ::Projects::ForkService.new(user_project, current_user, fork_params).execute - - if forked_project.errors.any? - conflict!(forked_project.errors.messages) - else - present forked_project, with: ::API::V3::Entities::Project, - user_can_admin_project: can?(current_user, :admin_project, forked_project) - end - end - - desc 'Update an existing project' do - success ::API::V3::Entities::Project - end - params do - optional :name, type: String, desc: 'The name of the project' - optional :default_branch, type: String, desc: 'The default branch of the project' - optional :path, type: String, desc: 'The path of the repository' - use :optional_params - at_least_one_of :name, :description, :issues_enabled, :merge_requests_enabled, - :wiki_enabled, :builds_enabled, :snippets_enabled, - :shared_runners_enabled, :resolve_outdated_diff_discussions, - :container_registry_enabled, :lfs_enabled, :public, :visibility_level, - :public_builds, :request_access_enabled, :only_allow_merge_if_build_succeeds, - :only_allow_merge_if_all_discussions_are_resolved, :path, - :default_branch - end - put ':id' do - authorize_admin_project - attrs = map_public_to_visibility_level(declared_params(include_missing: false)) - authorize! :rename_project, user_project if attrs[:name].present? - authorize! :change_visibility_level, user_project if attrs[:visibility_level].present? - - result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute - - if result[:status] == :success - present user_project, with: ::API::V3::Entities::Project, - user_can_admin_project: can?(current_user, :admin_project, user_project) - else - render_validation_error!(user_project) - end - end - - desc 'Archive a project' do - success ::API::V3::Entities::Project - end - post ':id/archive' do - authorize!(:archive_project, user_project) - - user_project.archive! - - present user_project, with: ::API::V3::Entities::Project - end - - desc 'Unarchive a project' do - success ::API::V3::Entities::Project - end - post ':id/unarchive' do - authorize!(:archive_project, user_project) - - user_project.unarchive! - - present user_project, with: ::API::V3::Entities::Project - end - - desc 'Star a project' do - success ::API::V3::Entities::Project - end - post ':id/star' do - if current_user.starred?(user_project) - not_modified! - else - current_user.toggle_star(user_project) - user_project.reload - - present user_project, with: ::API::V3::Entities::Project - end - end - - desc 'Unstar a project' do - success ::API::V3::Entities::Project - end - delete ':id/star' do - if current_user.starred?(user_project) - current_user.toggle_star(user_project) - user_project.reload - - present user_project, with: ::API::V3::Entities::Project - else - not_modified! - end - end - - desc 'Remove a project' - delete ":id" do - authorize! :remove_project, user_project - - status(200) - ::Projects::DestroyService.new(user_project, current_user, {}).async_execute - end - - desc 'Mark this project as forked from another' - params do - requires :forked_from_id, type: String, desc: 'The ID of the project it was forked from' - end - post ":id/fork/:forked_from_id" do - authenticated_as_admin! - - forked_from_project = find_project!(params[:forked_from_id]) - not_found!("Source Project") unless forked_from_project - - if user_project.forked_from_project.nil? - user_project.create_forked_project_link(forked_to_project_id: user_project.id, forked_from_project_id: forked_from_project.id) - - ::Projects::ForksCountService.new(forked_from_project).refresh_cache - else - render_api_error!("Project already forked", 409) - end - end - - desc 'Remove a forked_from relationship' - delete ":id/fork" do - authorize! :remove_fork_project, user_project - - if user_project.forked? - status(200) - user_project.forked_project_link.destroy - else - not_modified! - end - end - - desc 'Share the project with a group' do - success ::API::Entities::ProjectGroupLink - end - params do - requires :group_id, type: Integer, desc: 'The ID of a group' - requires :group_access, type: Integer, values: Gitlab::Access.values, desc: 'The group access level' - optional :expires_at, type: Date, desc: 'Share expiration date' - end - post ":id/share" do - authorize! :admin_project, user_project - group = Group.find_by_id(params[:group_id]) - - unless group && can?(current_user, :read_group, group) - not_found!('Group') - end - - unless user_project.allowed_to_share_with_group? - break render_api_error!("The project sharing with group is disabled", 400) - end - - link = user_project.project_group_links.new(declared_params(include_missing: false)) - - if link.save - present link, with: ::API::Entities::ProjectGroupLink - else - render_api_error!(link.errors.full_messages.first, 409) - end - end - - params do - requires :group_id, type: Integer, desc: 'The ID of the group' - end - delete ":id/share/:group_id" do - authorize! :admin_project, user_project - - link = user_project.project_group_links.find_by(group_id: params[:group_id]) - not_found!('Group Link') unless link - - link.destroy - no_content! - end - - desc 'Upload a file' - params do - requires :file, type: File, desc: 'The file to be uploaded' - end - post ":id/uploads" do - UploadService.new(user_project, params[:file]).execute - end - - desc 'Get the users list of a project' do - success ::API::Entities::UserBasic - end - params do - optional :search, type: String, desc: 'Return list of users matching the search criteria' - use :pagination - end - get ':id/users' do - users = user_project.team.users - users = users.search(params[:search]) if params[:search].present? - - present paginate(users), with: ::API::Entities::UserBasic - end - end - end - end -end diff --git a/lib/api/v3/repositories.rb b/lib/api/v3/repositories.rb deleted file mode 100644 index f701d64e886..00000000000 --- a/lib/api/v3/repositories.rb +++ /dev/null @@ -1,110 +0,0 @@ -require 'mime/types' - -module API - module V3 - class Repositories < Grape::API - before { authorize! :download_code, user_project } - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do - helpers do - def handle_project_member_errors(errors) - if errors[:project_access].any? - error!(errors[:project_access], 422) - end - - not_found! - end - end - - desc 'Get a project repository tree' do - success ::API::Entities::TreeObject - end - params do - optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used' - optional :path, type: String, desc: 'The path of the tree' - optional :recursive, type: Boolean, default: false, desc: 'Used to get a recursive tree' - end - get ':id/repository/tree' do - ref = params[:ref_name] || user_project.try(:default_branch) || 'master' - path = params[:path] || nil - - commit = user_project.commit(ref) - not_found!('Tree') unless commit - - tree = user_project.repository.tree(commit.id, path, recursive: params[:recursive]) - - present tree.sorted_entries, with: ::API::Entities::TreeObject - end - - desc 'Get a raw file contents' - params do - requires :sha, type: String, desc: 'The commit, branch name, or tag name' - requires :filepath, type: String, desc: 'The path to the file to display' - end - get [":id/repository/blobs/:sha", ":id/repository/commits/:sha/blob"], requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do - repo = user_project.repository - commit = repo.commit(params[:sha]) - not_found! "Commit" unless commit - blob = Gitlab::Git::Blob.find(repo, commit.id, params[:filepath]) - not_found! "File" unless blob - send_git_blob repo, blob - end - - desc 'Get a raw blob contents by blob sha' - params do - requires :sha, type: String, desc: 'The commit, branch name, or tag name' - end - get ':id/repository/raw_blobs/:sha', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do - repo = user_project.repository - begin - blob = Gitlab::Git::Blob.raw(repo, params[:sha]) - rescue - not_found! 'Blob' - end - not_found! 'Blob' unless blob - send_git_blob repo, blob - end - - desc 'Get an archive of the repository' - params do - optional :sha, type: String, desc: 'The commit sha of the archive to be downloaded' - optional :format, type: String, desc: 'The archive format' - end - get ':id/repository/archive', requirements: { format: Gitlab::PathRegex.archive_formats_regex } do - begin - send_git_archive user_project.repository, ref: params[:sha], format: params[:format], append_sha: true - rescue - not_found!('File') - end - end - - desc 'Compare two branches, tags, or commits' do - success ::API::Entities::Compare - end - params do - requires :from, type: String, desc: 'The commit, branch name, or tag name to start comparison' - requires :to, type: String, desc: 'The commit, branch name, or tag name to stop comparison' - end - get ':id/repository/compare' do - compare = Gitlab::Git::Compare.new(user_project.repository.raw_repository, params[:from], params[:to]) - present compare, with: ::API::Entities::Compare - end - - desc 'Get repository contributors' do - success ::API::Entities::Contributor - end - get ':id/repository/contributors' do - begin - present user_project.repository.contributors, - with: ::API::Entities::Contributor - rescue - not_found! - end - end - end - end - end -end diff --git a/lib/api/v3/runners.rb b/lib/api/v3/runners.rb deleted file mode 100644 index 8a5c46805bd..00000000000 --- a/lib/api/v3/runners.rb +++ /dev/null @@ -1,66 +0,0 @@ -module API - module V3 - class Runners < Grape::API - include PaginationParams - - before { authenticate! } - - resource :runners do - desc 'Remove a runner' do - success ::API::Entities::Runner - end - params do - requires :id, type: Integer, desc: 'The ID of the runner' - end - delete ':id' do - runner = Ci::Runner.find(params[:id]) - not_found!('Runner') unless runner - - authenticate_delete_runner!(runner) - - status(200) - runner.destroy - end - end - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - before { authorize_admin_project } - - desc "Disable project's runner" do - success ::API::Entities::Runner - end - params do - requires :runner_id, type: Integer, desc: 'The ID of the runner' - end - delete ':id/runners/:runner_id' do - runner_project = user_project.runner_projects.find_by(runner_id: params[:runner_id]) - not_found!('Runner') unless runner_project - - runner = runner_project.runner - forbidden!("Only one project associated with the runner. Please remove the runner instead") if runner.projects.count == 1 - - runner_project.destroy - - present runner, with: ::API::Entities::Runner - end - end - - helpers do - def authenticate_delete_runner!(runner) - return if current_user.admin? - - forbidden!("Runner is shared") if runner.is_shared? - forbidden!("Runner associated with more than one project") if runner.projects.count > 1 - forbidden!("No access granted") unless user_can_access_runner?(runner) - end - - def user_can_access_runner?(runner) - current_user.ci_owned_runners.exists?(runner.id) - end - end - end - end -end diff --git a/lib/api/v3/services.rb b/lib/api/v3/services.rb deleted file mode 100644 index 20ca1021c71..00000000000 --- a/lib/api/v3/services.rb +++ /dev/null @@ -1,670 +0,0 @@ -module API - module V3 - class Services < Grape::API - services = { - 'asana' => [ - { - required: true, - name: :api_key, - type: String, - desc: 'User API token' - }, - { - required: false, - name: :restrict_to_branch, - type: String, - desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches' - } - ], - 'assembla' => [ - { - required: true, - name: :token, - type: String, - desc: 'The authentication token' - }, - { - required: false, - name: :subdomain, - type: String, - desc: 'Subdomain setting' - } - ], - 'bamboo' => [ - { - required: true, - name: :bamboo_url, - type: String, - desc: 'Bamboo root URL like https://bamboo.example.com' - }, - { - required: true, - name: :build_key, - type: String, - desc: 'Bamboo build plan key like' - }, - { - required: true, - name: :username, - type: String, - desc: 'A user with API access, if applicable' - }, - { - required: true, - name: :password, - type: String, - desc: 'Passord of the user' - } - ], - 'bugzilla' => [ - { - required: true, - name: :new_issue_url, - type: String, - desc: 'New issue URL' - }, - { - required: true, - name: :issues_url, - type: String, - desc: 'Issues URL' - }, - { - required: true, - name: :project_url, - type: String, - desc: 'Project URL' - }, - { - required: false, - name: :description, - type: String, - desc: 'Description' - }, - { - required: false, - name: :title, - type: String, - desc: 'Title' - } - ], - 'buildkite' => [ - { - required: true, - name: :token, - type: String, - desc: 'Buildkite project GitLab token' - }, - { - required: true, - name: :project_url, - type: String, - desc: 'The buildkite project URL' - }, - { - required: false, - name: :enable_ssl_verification, - type: Boolean, - desc: 'Enable SSL verification for communication' - } - ], - 'builds-email' => [ - { - required: true, - name: :recipients, - type: String, - desc: 'Comma-separated list of recipient email addresses' - }, - { - required: false, - name: :add_pusher, - type: Boolean, - desc: 'Add pusher to recipients list' - }, - { - required: false, - name: :notify_only_broken_builds, - type: Boolean, - desc: 'Notify only broken builds' - } - ], - 'campfire' => [ - { - required: true, - name: :token, - type: String, - desc: 'Campfire token' - }, - { - required: false, - name: :subdomain, - type: String, - desc: 'Campfire subdomain' - }, - { - required: false, - name: :room, - type: String, - desc: 'Campfire room' - } - ], - 'custom-issue-tracker' => [ - { - required: true, - name: :new_issue_url, - type: String, - desc: 'New issue URL' - }, - { - required: true, - name: :issues_url, - type: String, - desc: 'Issues URL' - }, - { - required: true, - name: :project_url, - type: String, - desc: 'Project URL' - }, - { - required: false, - name: :description, - type: String, - desc: 'Description' - }, - { - required: false, - name: :title, - type: String, - desc: 'Title' - } - ], - 'drone-ci' => [ - { - required: true, - name: :token, - type: String, - desc: 'Drone CI token' - }, - { - required: true, - name: :drone_url, - type: String, - desc: 'Drone CI URL' - }, - { - required: false, - name: :enable_ssl_verification, - type: Boolean, - desc: 'Enable SSL verification for communication' - } - ], - 'emails-on-push' => [ - { - required: true, - name: :recipients, - type: String, - desc: 'Comma-separated list of recipient email addresses' - }, - { - required: false, - name: :disable_diffs, - type: Boolean, - desc: 'Disable code diffs' - }, - { - required: false, - name: :send_from_committer_email, - type: Boolean, - desc: 'Send from committer' - } - ], - 'external-wiki' => [ - { - required: true, - name: :external_wiki_url, - type: String, - desc: 'The URL of the external Wiki' - } - ], - 'flowdock' => [ - { - required: true, - name: :token, - type: String, - desc: 'Flowdock token' - } - ], - 'gemnasium' => [ - { - required: true, - name: :api_key, - type: String, - desc: 'Your personal API key on gemnasium.com' - }, - { - required: true, - name: :token, - type: String, - desc: "The project's slug on gemnasium.com" - } - ], - 'hipchat' => [ - { - required: true, - name: :token, - type: String, - desc: 'The room token' - }, - { - required: false, - name: :room, - type: String, - desc: 'The room name or ID' - }, - { - required: false, - name: :color, - type: String, - desc: 'The room color' - }, - { - required: false, - name: :notify, - type: Boolean, - desc: 'Enable notifications' - }, - { - required: false, - name: :api_version, - type: String, - desc: 'Leave blank for default (v2)' - }, - { - required: false, - name: :server, - type: String, - desc: 'Leave blank for default. https://hipchat.example.com' - } - ], - 'irker' => [ - { - required: true, - name: :recipients, - type: String, - desc: 'Recipients/channels separated by whitespaces' - }, - { - required: false, - name: :default_irc_uri, - type: String, - desc: 'Default: irc://irc.network.net:6697' - }, - { - required: false, - name: :server_host, - type: String, - desc: 'Server host. Default localhost' - }, - { - required: false, - name: :server_port, - type: Integer, - desc: 'Server port. Default 6659' - }, - { - required: false, - name: :colorize_messages, - type: Boolean, - desc: 'Colorize messages' - } - ], - 'jira' => [ - { - required: true, - name: :url, - type: String, - desc: 'The URL to the JIRA project which is being linked to this GitLab project, e.g., https://jira.example.com' - }, - { - required: true, - name: :project_key, - type: String, - desc: 'The short identifier for your JIRA project, all uppercase, e.g., PROJ' - }, - { - required: false, - name: :username, - type: String, - desc: 'The username of the user created to be used with GitLab/JIRA' - }, - { - required: false, - name: :password, - type: String, - desc: 'The password of the user created to be used with GitLab/JIRA' - }, - { - required: false, - name: :jira_issue_transition_id, - type: Integer, - desc: 'The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`' - } - ], - - 'kubernetes' => [ - { - required: true, - name: :namespace, - type: String, - desc: 'The Kubernetes namespace to use' - }, - { - required: true, - name: :api_url, - type: String, - desc: 'The URL to the Kubernetes cluster API, e.g., https://kubernetes.example.com' - }, - { - required: true, - name: :token, - type: String, - desc: 'The service token to authenticate against the Kubernetes cluster with' - }, - { - required: false, - name: :ca_pem, - type: String, - desc: 'A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)' - } - ], - 'mattermost-slash-commands' => [ - { - required: true, - name: :token, - type: String, - desc: 'The Mattermost token' - } - ], - 'slack-slash-commands' => [ - { - required: true, - name: :token, - type: String, - desc: 'The Slack token' - } - ], - 'packagist' => [ - { - required: true, - name: :username, - type: String, - desc: 'The username' - }, - { - required: true, - name: :token, - type: String, - desc: 'The Packagist API token' - }, - { - required: false, - name: :server, - type: String, - desc: 'The server' - } - ], - 'pipelines-email' => [ - { - required: true, - name: :recipients, - type: String, - desc: 'Comma-separated list of recipient email addresses' - }, - { - required: false, - name: :notify_only_broken_builds, - type: Boolean, - desc: 'Notify only broken builds' - } - ], - 'pivotaltracker' => [ - { - required: true, - name: :token, - type: String, - desc: 'The Pivotaltracker token' - }, - { - required: false, - name: :restrict_to_branch, - type: String, - desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.' - } - ], - 'pushover' => [ - { - required: true, - name: :api_key, - type: String, - desc: 'The application key' - }, - { - required: true, - name: :user_key, - type: String, - desc: 'The user key' - }, - { - required: true, - name: :priority, - type: String, - desc: 'The priority' - }, - { - required: true, - name: :device, - type: String, - desc: 'Leave blank for all active devices' - }, - { - required: true, - name: :sound, - type: String, - desc: 'The sound of the notification' - } - ], - 'redmine' => [ - { - required: true, - name: :new_issue_url, - type: String, - desc: 'The new issue URL' - }, - { - required: true, - name: :project_url, - type: String, - desc: 'The project URL' - }, - { - required: true, - name: :issues_url, - type: String, - desc: 'The issues URL' - }, - { - required: false, - name: :description, - type: String, - desc: 'The description of the tracker' - } - ], - 'slack' => [ - { - required: true, - name: :webhook, - type: String, - desc: 'The Slack webhook. e.g. https://hooks.slack.com/services/...' - }, - { - required: false, - name: :new_issue_url, - type: String, - desc: 'The user name' - }, - { - required: false, - name: :channel, - type: String, - desc: 'The channel name' - } - ], - 'microsoft-teams' => [ - required: true, - name: :webhook, - type: String, - desc: 'The Microsoft Teams webhook. e.g. https://outlook.office.com/webhook/…' - ], - 'mattermost' => [ - { - required: true, - name: :webhook, - type: String, - desc: 'The Mattermost webhook. e.g. http://mattermost_host/hooks/...' - } - ], - 'teamcity' => [ - { - required: true, - name: :teamcity_url, - type: String, - desc: 'TeamCity root URL like https://teamcity.example.com' - }, - { - required: true, - name: :build_type, - type: String, - desc: 'Build configuration ID' - }, - { - required: true, - name: :username, - type: String, - desc: 'A user with permissions to trigger a manual build' - }, - { - required: true, - name: :password, - type: String, - desc: 'The password of the user' - } - ] - } - - trigger_services = { - 'mattermost-slash-commands' => [ - { - name: :token, - type: String, - desc: 'The Mattermost token' - } - ], - 'slack-slash-commands' => [ - { - name: :token, - type: String, - desc: 'The Slack token' - } - ] - }.freeze - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - before { authenticate! } - before { authorize_admin_project } - - helpers do - def service_attributes(service) - service.fields.inject([]) do |arr, hash| - arr << hash[:name].to_sym - end - end - end - - desc "Delete a service for project" - params do - requires :service_slug, type: String, values: services.keys, desc: 'The name of the service' - end - delete ":id/services/:service_slug" do - service = user_project.find_or_initialize_service(params[:service_slug].underscore) - - attrs = service_attributes(service).inject({}) do |hash, key| - hash.merge!(key => nil) - end - - if service.update_attributes(attrs.merge(active: false)) - status(200) - true - else - render_api_error!('400 Bad Request', 400) - end - end - - desc 'Get the service settings for project' do - success Entities::ProjectService - end - params do - requires :service_slug, type: String, values: services.keys, desc: 'The name of the service' - end - get ":id/services/:service_slug" do - service = user_project.find_or_initialize_service(params[:service_slug].underscore) - present service, with: Entities::ProjectService - end - end - - trigger_services.each do |service_slug, settings| - helpers do - def slash_command_service(project, service_slug, params) - project.services.active.where(template: false).find do |service| - service.try(:token) == params[:token] && service.to_param == service_slug.underscore - end - end - end - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - desc "Trigger a slash command for #{service_slug}" do - detail 'Added in GitLab 8.13' - end - params do - settings.each do |setting| - requires setting[:name], type: setting[:type], desc: setting[:desc] - end - end - post ":id/services/#{service_slug.underscore}/trigger" do - project = find_project(params[:id]) - - # This is not accurate, but done to prevent leakage of the project names - not_found!('Service') unless project - - service = slash_command_service(project, service_slug, params) - result = service.try(:trigger, params) - - if result - status result[:status] || 200 - present result - else - not_found!('Service') - end - end - end - end - end - end -end diff --git a/lib/api/v3/settings.rb b/lib/api/v3/settings.rb deleted file mode 100644 index fc56495c8b1..00000000000 --- a/lib/api/v3/settings.rb +++ /dev/null @@ -1,147 +0,0 @@ -module API - module V3 - class Settings < Grape::API - before { authenticated_as_admin! } - - helpers do - def current_settings - @current_setting ||= - (ApplicationSetting.current_without_cache || ApplicationSetting.create_from_defaults) - end - end - - desc 'Get the current application settings' do - success Entities::ApplicationSetting - end - get "application/settings" do - present current_settings, with: Entities::ApplicationSetting - end - - desc 'Modify application settings' do - success Entities::ApplicationSetting - end - params do - optional :default_branch_protection, type: Integer, values: [0, 1, 2], desc: 'Determine if developers can push to master' - optional :default_project_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default project visibility' - optional :default_snippet_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default snippet visibility' - optional :default_group_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default group visibility' - optional :restricted_visibility_levels, type: Array[String], desc: 'Selected levels cannot be used by non-admin users for projects or snippets. If the public level is restricted, user profiles are only visible to logged in users.' - optional :import_sources, type: Array[String], values: %w[github bitbucket gitlab google_code fogbugz git gitlab_project], - desc: 'Enabled sources for code import during project creation. OmniAuth must be configured for GitHub, Bitbucket, and GitLab.com' - optional :disabled_oauth_sign_in_sources, type: Array[String], desc: 'Disable certain OAuth sign-in sources' - optional :enabled_git_access_protocol, type: String, values: %w[ssh http nil], desc: 'Allow only the selected protocols to be used for Git access.' - optional :gravatar_enabled, type: Boolean, desc: 'Flag indicating if the Gravatar service is enabled' - optional :default_projects_limit, type: Integer, desc: 'The maximum number of personal projects' - optional :max_attachment_size, type: Integer, desc: 'Maximum attachment size in MB' - optional :session_expire_delay, type: Integer, desc: 'Session duration in minutes. GitLab restart is required to apply changes.' - optional :user_oauth_applications, type: Boolean, desc: 'Allow users to register any application to use GitLab as an OAuth provider' - optional :user_default_external, type: Boolean, desc: 'Newly registered users will by default be external' - optional :signup_enabled, type: Boolean, desc: 'Flag indicating if sign up is enabled' - optional :send_user_confirmation_email, type: Boolean, desc: 'Send confirmation email on sign-up' - optional :domain_whitelist, type: String, desc: 'ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com' - optional :domain_blacklist_enabled, type: Boolean, desc: 'Enable domain blacklist for sign ups' - given domain_blacklist_enabled: ->(val) { val } do - requires :domain_blacklist, type: String, desc: 'Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com' - end - optional :after_sign_up_text, type: String, desc: 'Text shown after sign up' - optional :password_authentication_enabled, type: Boolean, desc: 'Flag indicating if password authentication is enabled for the web interface' - optional :signin_enabled, type: Boolean, desc: 'Flag indicating if password authentication is enabled for the web interface' - mutually_exclusive :password_authentication_enabled, :signin_enabled - optional :require_two_factor_authentication, type: Boolean, desc: 'Require all users to setup Two-factor authentication' - given require_two_factor_authentication: ->(val) { val } do - requires :two_factor_grace_period, type: Integer, desc: 'Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication' - end - optional :home_page_url, type: String, desc: 'We will redirect non-logged in users to this page' - optional :after_sign_out_path, type: String, desc: 'We will redirect users to this page after they sign out' - optional :sign_in_text, type: String, desc: 'The sign in text of the GitLab application' - optional :help_page_text, type: String, desc: 'Custom text displayed on the help page' - optional :shared_runners_enabled, type: Boolean, desc: 'Enable shared runners for new projects' - given shared_runners_enabled: ->(val) { val } do - requires :shared_runners_text, type: String, desc: 'Shared runners text ' - end - optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size each build's artifacts can have" - optional :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB' - optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)' - optional :metrics_enabled, type: Boolean, desc: 'Enable the InfluxDB metrics' - given metrics_enabled: ->(val) { val } do - requires :metrics_host, type: String, desc: 'The InfluxDB host' - requires :metrics_port, type: Integer, desc: 'The UDP port to use for connecting to InfluxDB' - requires :metrics_pool_size, type: Integer, desc: 'The amount of InfluxDB connections to open' - requires :metrics_timeout, type: Integer, desc: 'The amount of seconds after which an InfluxDB connection will time out' - requires :metrics_method_call_threshold, type: Integer, desc: 'A method call is only tracked when it takes longer to complete than the given amount of milliseconds.' - requires :metrics_sample_interval, type: Integer, desc: 'The sampling interval in seconds' - requires :metrics_packet_size, type: Integer, desc: 'The amount of points to store in a single UDP packet' - end - optional :sidekiq_throttling_enabled, type: Boolean, desc: 'Enable Sidekiq Job Throttling' - given sidekiq_throttling_enabled: ->(val) { val } do - requires :sidekiq_throttling_queus, type: Array[String], desc: 'Choose which queues you wish to throttle' - requires :sidekiq_throttling_factor, type: Float, desc: 'The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.' - end - optional :recaptcha_enabled, type: Boolean, desc: 'Helps prevent bots from creating accounts' - given recaptcha_enabled: ->(val) { val } do - requires :recaptcha_site_key, type: String, desc: 'Generate site key at http://www.google.com/recaptcha' - requires :recaptcha_private_key, type: String, desc: 'Generate private key at http://www.google.com/recaptcha' - end - optional :akismet_enabled, type: Boolean, desc: 'Helps prevent bots from creating issues' - given akismet_enabled: ->(val) { val } do - requires :akismet_api_key, type: String, desc: 'Generate API key at http://www.akismet.com' - end - optional :admin_notification_email, type: String, desc: 'Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.' - optional :sentry_enabled, type: Boolean, desc: 'Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here: https://getsentry.com' - given sentry_enabled: ->(val) { val } do - requires :sentry_dsn, type: String, desc: 'Sentry Data Source Name' - end - optional :repository_storage, type: String, desc: 'Storage paths for new projects' - optional :repository_checks_enabled, type: Boolean, desc: "GitLab will periodically run 'git fsck' in all project and wiki repositories to look for silent disk corruption issues." - optional :koding_enabled, type: Boolean, desc: 'Enable Koding' - given koding_enabled: ->(val) { val } do - requires :koding_url, type: String, desc: 'The Koding team URL' - end - optional :plantuml_enabled, type: Boolean, desc: 'Enable PlantUML' - given plantuml_enabled: ->(val) { val } do - requires :plantuml_url, type: String, desc: 'The PlantUML server URL' - end - optional :version_check_enabled, type: Boolean, desc: 'Let GitLab inform you when an update is available.' - optional :email_author_in_body, type: Boolean, desc: 'Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead.' - optional :html_emails_enabled, type: Boolean, desc: 'By default GitLab sends emails in HTML and plain text formats so mail clients can choose what format to use. Disable this option if you only want to send emails in plain text format.' - optional :housekeeping_enabled, type: Boolean, desc: 'Enable automatic repository housekeeping (git repack, git gc)' - given housekeeping_enabled: ->(val) { val } do - requires :housekeeping_bitmaps_enabled, type: Boolean, desc: "Creating pack file bitmaps makes housekeeping take a little longer but bitmaps should accelerate 'git clone' performance." - requires :housekeeping_incremental_repack_period, type: Integer, desc: "Number of Git pushes after which an incremental 'git repack' is run." - requires :housekeeping_full_repack_period, type: Integer, desc: "Number of Git pushes after which a full 'git repack' is run." - requires :housekeeping_gc_period, type: Integer, desc: "Number of Git pushes after which 'git gc' is run." - end - optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.' - at_least_one_of :default_branch_protection, :default_project_visibility, :default_snippet_visibility, - :default_group_visibility, :restricted_visibility_levels, :import_sources, - :enabled_git_access_protocol, :gravatar_enabled, :default_projects_limit, - :max_attachment_size, :session_expire_delay, :disabled_oauth_sign_in_sources, - :user_oauth_applications, :user_default_external, :signup_enabled, - :send_user_confirmation_email, :domain_whitelist, :domain_blacklist_enabled, - :after_sign_up_text, :password_authentication_enabled, :signin_enabled, :require_two_factor_authentication, - :home_page_url, :after_sign_out_path, :sign_in_text, :help_page_text, - :shared_runners_enabled, :max_artifacts_size, :max_pages_size, :container_registry_token_expire_delay, - :metrics_enabled, :sidekiq_throttling_enabled, :recaptcha_enabled, - :akismet_enabled, :admin_notification_email, :sentry_enabled, - :repository_storage, :repository_checks_enabled, :koding_enabled, :plantuml_enabled, - :version_check_enabled, :email_author_in_body, :html_emails_enabled, - :housekeeping_enabled, :terminal_max_session_time - end - put "application/settings" do - attrs = declared_params(include_missing: false) - - if attrs.has_key?(:signin_enabled) - attrs[:password_authentication_enabled_for_web] = attrs.delete(:signin_enabled) - elsif attrs.has_key?(:password_authentication_enabled) - attrs[:password_authentication_enabled_for_web] = attrs.delete(:password_authentication_enabled) - end - - if current_settings.update_attributes(attrs) - present current_settings, with: Entities::ApplicationSetting - else - render_validation_error!(current_settings) - end - end - end - end -end diff --git a/lib/api/v3/snippets.rb b/lib/api/v3/snippets.rb deleted file mode 100644 index 1df8a20e74a..00000000000 --- a/lib/api/v3/snippets.rb +++ /dev/null @@ -1,141 +0,0 @@ -module API - module V3 - class Snippets < Grape::API - include PaginationParams - - before { authenticate! } - - resource :snippets do - helpers do - def snippets_for_current_user - SnippetsFinder.new(current_user, author: current_user).execute - end - - def public_snippets - SnippetsFinder.new(current_user, visibility: Snippet::PUBLIC).execute - end - end - - desc 'Get a snippets list for authenticated user' do - detail 'This feature was introduced in GitLab 8.15.' - success ::API::Entities::PersonalSnippet - end - params do - use :pagination - end - get do - present paginate(snippets_for_current_user), with: ::API::Entities::PersonalSnippet - end - - desc 'List all public snippets current_user has access to' do - detail 'This feature was introduced in GitLab 8.15.' - success ::API::Entities::PersonalSnippet - end - params do - use :pagination - end - get 'public' do - present paginate(public_snippets), with: ::API::Entities::PersonalSnippet - end - - desc 'Get a single snippet' do - detail 'This feature was introduced in GitLab 8.15.' - success ::API::Entities::PersonalSnippet - end - params do - requires :id, type: Integer, desc: 'The ID of a snippet' - end - get ':id' do - snippet = snippets_for_current_user.find(params[:id]) - present snippet, with: ::API::Entities::PersonalSnippet - end - - desc 'Create new snippet' do - detail 'This feature was introduced in GitLab 8.15.' - success ::API::Entities::PersonalSnippet - end - params do - requires :title, type: String, desc: 'The title of a snippet' - requires :file_name, type: String, desc: 'The name of a snippet file' - requires :content, type: String, desc: 'The content of a snippet' - optional :visibility_level, type: Integer, - values: Gitlab::VisibilityLevel.values, - default: Gitlab::VisibilityLevel::INTERNAL, - desc: 'The visibility level of the snippet' - end - post do - attrs = declared_params(include_missing: false).merge(request: request, api: true) - snippet = CreateSnippetService.new(nil, current_user, attrs).execute - - if snippet.persisted? - present snippet, with: ::API::Entities::PersonalSnippet - else - render_validation_error!(snippet) - end - end - - desc 'Update an existing snippet' do - detail 'This feature was introduced in GitLab 8.15.' - success ::API::Entities::PersonalSnippet - end - params do - requires :id, type: Integer, desc: 'The ID of a snippet' - optional :title, type: String, desc: 'The title of a snippet' - optional :file_name, type: String, desc: 'The name of a snippet file' - optional :content, type: String, desc: 'The content of a snippet' - optional :visibility_level, type: Integer, - values: Gitlab::VisibilityLevel.values, - desc: 'The visibility level of the snippet' - at_least_one_of :title, :file_name, :content, :visibility_level - end - put ':id' do - snippet = snippets_for_current_user.find_by(id: params.delete(:id)) - break not_found!('Snippet') unless snippet - - authorize! :update_personal_snippet, snippet - - attrs = declared_params(include_missing: false) - - UpdateSnippetService.new(nil, current_user, snippet, attrs).execute - - if snippet.persisted? - present snippet, with: ::API::Entities::PersonalSnippet - else - render_validation_error!(snippet) - end - end - - desc 'Remove snippet' do - detail 'This feature was introduced in GitLab 8.15.' - success ::API::Entities::PersonalSnippet - end - params do - requires :id, type: Integer, desc: 'The ID of a snippet' - end - delete ':id' do - snippet = snippets_for_current_user.find_by(id: params.delete(:id)) - break not_found!('Snippet') unless snippet - - authorize! :destroy_personal_snippet, snippet - snippet.destroy - no_content! - end - - desc 'Get a raw snippet' do - detail 'This feature was introduced in GitLab 8.15.' - end - params do - requires :id, type: Integer, desc: 'The ID of a snippet' - end - get ":id/raw" do - snippet = snippets_for_current_user.find_by(id: params.delete(:id)) - break not_found!('Snippet') unless snippet - - env['api.format'] = :txt - content_type 'text/plain' - present snippet.content - end - end - end - end -end diff --git a/lib/api/v3/subscriptions.rb b/lib/api/v3/subscriptions.rb deleted file mode 100644 index 690768db82f..00000000000 --- a/lib/api/v3/subscriptions.rb +++ /dev/null @@ -1,53 +0,0 @@ -module API - module V3 - class Subscriptions < Grape::API - before { authenticate! } - - subscribable_types = { - 'merge_request' => proc { |id| find_merge_request_with_access(id, :update_merge_request) }, - 'merge_requests' => proc { |id| find_merge_request_with_access(id, :update_merge_request) }, - 'issues' => proc { |id| find_project_issue(id) }, - 'labels' => proc { |id| find_project_label(id) } - } - - params do - requires :id, type: String, desc: 'The ID of a project' - requires :subscribable_id, type: String, desc: 'The ID of a resource' - end - resource :projects, requirements: { id: %r{[^/]+} } do - subscribable_types.each do |type, finder| - type_singularized = type.singularize - entity_class = ::API::Entities.const_get(type_singularized.camelcase) - - desc 'Subscribe to a resource' do - success entity_class - end - post ":id/#{type}/:subscribable_id/subscription" do - resource = instance_exec(params[:subscribable_id], &finder) - - if resource.subscribed?(current_user, user_project) - not_modified! - else - resource.subscribe(current_user, user_project) - present resource, with: entity_class, current_user: current_user, project: user_project - end - end - - desc 'Unsubscribe from a resource' do - success entity_class - end - delete ":id/#{type}/:subscribable_id/subscription" do - resource = instance_exec(params[:subscribable_id], &finder) - - if !resource.subscribed?(current_user, user_project) - not_modified! - else - resource.unsubscribe(current_user, user_project) - present resource, with: entity_class, current_user: current_user, project: user_project - end - end - end - end - end - end -end diff --git a/lib/api/v3/system_hooks.rb b/lib/api/v3/system_hooks.rb deleted file mode 100644 index 5787c06fc12..00000000000 --- a/lib/api/v3/system_hooks.rb +++ /dev/null @@ -1,32 +0,0 @@ -module API - module V3 - class SystemHooks < Grape::API - before do - authenticate! - authenticated_as_admin! - end - - resource :hooks do - desc 'Get the list of system hooks' do - success ::API::Entities::Hook - end - get do - present SystemHook.all, with: ::API::Entities::Hook - end - - desc 'Delete a hook' do - success ::API::Entities::Hook - end - params do - requires :id, type: Integer, desc: 'The ID of the system hook' - end - delete ":id" do - hook = SystemHook.find_by(id: params[:id]) - not_found!('System hook') unless hook - - present hook.destroy, with: ::API::Entities::Hook - end - end - end - end -end diff --git a/lib/api/v3/tags.rb b/lib/api/v3/tags.rb deleted file mode 100644 index 6e37d31d153..00000000000 --- a/lib/api/v3/tags.rb +++ /dev/null @@ -1,40 +0,0 @@ -module API - module V3 - class Tags < Grape::API - before { authorize! :download_code, user_project } - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Get a project repository tags' do - success ::API::Entities::Tag - end - get ":id/repository/tags" do - tags = user_project.repository.tags.sort_by(&:name).reverse - present tags, with: ::API::Entities::Tag, project: user_project - end - - desc 'Delete a repository tag' - params do - requires :tag_name, type: String, desc: 'The name of the tag' - end - delete ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do - authorize_push_project - - result = ::Tags::DestroyService.new(user_project, current_user) - .execute(params[:tag_name]) - - if result[:status] == :success - status(200) - { - tag_name: params[:tag_name] - } - else - render_api_error!(result[:message], result[:return_code]) - end - end - end - end - end -end diff --git a/lib/api/v3/templates.rb b/lib/api/v3/templates.rb deleted file mode 100644 index b82b02b5f49..00000000000 --- a/lib/api/v3/templates.rb +++ /dev/null @@ -1,122 +0,0 @@ -module API - module V3 - class Templates < Grape::API - GLOBAL_TEMPLATE_TYPES = { - gitignores: { - klass: Gitlab::Template::GitignoreTemplate, - gitlab_version: 8.8 - }, - gitlab_ci_ymls: { - klass: Gitlab::Template::GitlabCiYmlTemplate, - gitlab_version: 8.9 - }, - dockerfiles: { - klass: Gitlab::Template::DockerfileTemplate, - gitlab_version: 8.15 - } - }.freeze - PROJECT_TEMPLATE_REGEX = - %r{[\<\{\[] - (project|description| - one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here - [\>\}\]]}xi.freeze - YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze - FULLNAME_TEMPLATE_REGEX = - %r{[\<\{\[] - (fullname|name\sof\s(author|copyright\sowner)) - [\>\}\]]}xi.freeze - DEPRECATION_MESSAGE = ' This endpoint is deprecated and has been removed in V4.'.freeze - - helpers do - def parsed_license_template - # We create a fresh Licensee::License object since we'll modify its - # content in place below. - template = Licensee::License.new(params[:name]) - - template.content.gsub!(YEAR_TEMPLATE_REGEX, Time.now.year.to_s) - template.content.gsub!(PROJECT_TEMPLATE_REGEX, params[:project]) if params[:project].present? - - fullname = params[:fullname].presence || current_user.try(:name) - template.content.gsub!(FULLNAME_TEMPLATE_REGEX, fullname) if fullname - template - end - - def render_response(template_type, template) - not_found!(template_type.to_s.singularize) unless template - present template, with: ::API::Entities::Template - end - end - - { "licenses" => :deprecated, "templates/licenses" => :ok }.each do |route, status| - desc 'Get the list of the available license template' do - detailed_desc = 'This feature was introduced in GitLab 8.7.' - detailed_desc << DEPRECATION_MESSAGE unless status == :ok - detail detailed_desc - success ::API::Entities::License - end - params do - optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses' - end - get route do - options = { - featured: declared(params)[:popular].present? ? true : nil - } - present Licensee::License.all(options), with: ::API::Entities::License - end - end - - { "licenses/:name" => :deprecated, "templates/licenses/:name" => :ok }.each do |route, status| - desc 'Get the text for a specific license' do - detailed_desc = 'This feature was introduced in GitLab 8.7.' - detailed_desc << DEPRECATION_MESSAGE unless status == :ok - detail detailed_desc - success ::API::Entities::License - end - params do - requires :name, type: String, desc: 'The name of the template' - end - get route, requirements: { name: /[\w\.-]+/ } do - not_found!('License') unless Licensee::License.find(declared(params)[:name]) - - template = parsed_license_template - - present template, with: ::API::Entities::License - end - end - - GLOBAL_TEMPLATE_TYPES.each do |template_type, properties| - klass = properties[:klass] - gitlab_version = properties[:gitlab_version] - - { template_type => :deprecated, "templates/#{template_type}" => :ok }.each do |route, status| - desc 'Get the list of the available template' do - detailed_desc = "This feature was introduced in GitLab #{gitlab_version}." - detailed_desc << DEPRECATION_MESSAGE unless status == :ok - detail detailed_desc - success ::API::Entities::TemplatesList - end - get route do - present klass.all, with: ::API::Entities::TemplatesList - end - end - - { "#{template_type}/:name" => :deprecated, "templates/#{template_type}/:name" => :ok }.each do |route, status| - desc 'Get the text for a specific template present in local filesystem' do - detailed_desc = "This feature was introduced in GitLab #{gitlab_version}." - detailed_desc << DEPRECATION_MESSAGE unless status == :ok - detail detailed_desc - success ::API::Entities::Template - end - params do - requires :name, type: String, desc: 'The name of the template' - end - get route do - new_template = klass.find(declared(params)[:name]) - - render_response(template_type, new_template) - end - end - end - end - end -end diff --git a/lib/api/v3/time_tracking_endpoints.rb b/lib/api/v3/time_tracking_endpoints.rb deleted file mode 100644 index 1aad39815f9..00000000000 --- a/lib/api/v3/time_tracking_endpoints.rb +++ /dev/null @@ -1,116 +0,0 @@ -module API - module V3 - module TimeTrackingEndpoints - extend ActiveSupport::Concern - - included do - helpers do - def issuable_name - declared_params.key?(:issue_id) ? 'issue' : 'merge_request' - end - - def issuable_key - "#{issuable_name}_id".to_sym - end - - def update_issuable_key - "update_#{issuable_name}".to_sym - end - - def read_issuable_key - "read_#{issuable_name}".to_sym - end - - def load_issuable - @issuable ||= begin - case issuable_name - when 'issue' - find_project_issue(params.delete(issuable_key)) - when 'merge_request' - find_project_merge_request(params.delete(issuable_key)) - end - end - end - - def update_issuable(attrs) - custom_params = declared_params(include_missing: false) - custom_params.merge!(attrs) - - issuable = update_service.new(user_project, current_user, custom_params).execute(load_issuable) - if issuable.valid? - present issuable, with: ::API::Entities::IssuableTimeStats - else - render_validation_error!(issuable) - end - end - - def update_service - issuable_name == 'issue' ? ::Issues::UpdateService : ::MergeRequests::UpdateService - end - end - - issuable_name = name.end_with?('Issues') ? 'issue' : 'merge_request' - issuable_collection_name = issuable_name.pluralize - issuable_key = "#{issuable_name}_id".to_sym - - desc "Set a time estimate for a project #{issuable_name}" - params do - requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" - requires :duration, type: String, desc: 'The duration to be parsed' - end - post ":id/#{issuable_collection_name}/:#{issuable_key}/time_estimate" do - authorize! update_issuable_key, load_issuable - - status :ok - update_issuable(time_estimate: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration))) - end - - desc "Reset the time estimate for a project #{issuable_name}" - params do - requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" - end - post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_time_estimate" do - authorize! update_issuable_key, load_issuable - - status :ok - update_issuable(time_estimate: 0) - end - - desc "Add spent time for a project #{issuable_name}" - params do - requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" - requires :duration, type: String, desc: 'The duration to be parsed' - end - post ":id/#{issuable_collection_name}/:#{issuable_key}/add_spent_time" do - authorize! update_issuable_key, load_issuable - - update_issuable(spend_time: { - duration: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)), - user_id: current_user.id - }) - end - - desc "Reset spent time for a project #{issuable_name}" - params do - requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" - end - post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_spent_time" do - authorize! update_issuable_key, load_issuable - - status :ok - update_issuable(spend_time: { duration: :reset, user_id: current_user.id }) - end - - desc "Show time stats for a project #{issuable_name}" - params do - requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" - end - get ":id/#{issuable_collection_name}/:#{issuable_key}/time_stats" do - authorize! read_issuable_key, load_issuable - - present load_issuable, with: ::API::Entities::IssuableTimeStats - end - end - end - end -end diff --git a/lib/api/v3/todos.rb b/lib/api/v3/todos.rb deleted file mode 100644 index 3e2c61f6dbd..00000000000 --- a/lib/api/v3/todos.rb +++ /dev/null @@ -1,30 +0,0 @@ -module API - module V3 - class Todos < Grape::API - before { authenticate! } - - resource :todos do - desc 'Mark a todo as done' do - success ::API::Entities::Todo - end - params do - requires :id, type: Integer, desc: 'The ID of the todo being marked as done' - end - delete ':id' do - TodoService.new.mark_todos_as_done_by_ids(params[:id], current_user) - todo = current_user.todos.find(params[:id]) - - present todo, with: ::API::Entities::Todo, current_user: current_user - end - - desc 'Mark all todos as done' - delete do - status(200) - - todos = TodosFinder.new(current_user, params).execute - TodoService.new.mark_todos_as_done(todos, current_user).size - end - end - end - end -end diff --git a/lib/api/v3/triggers.rb b/lib/api/v3/triggers.rb deleted file mode 100644 index 969bb2a05de..00000000000 --- a/lib/api/v3/triggers.rb +++ /dev/null @@ -1,112 +0,0 @@ -module API - module V3 - class Triggers < Grape::API - include PaginationParams - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Trigger a GitLab project build' do - success ::API::V3::Entities::TriggerRequest - end - params do - requires :ref, type: String, desc: 'The commit sha or name of a branch or tag' - requires :token, type: String, desc: 'The unique token of trigger' - optional :variables, type: Hash, desc: 'The list of variables to be injected into build' - end - post ":id/(ref/:ref/)trigger/builds", requirements: { ref: /.+/ } do - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42121') - - # validate variables - params[:variables] = params[:variables].to_h - unless params[:variables].all? { |key, value| key.is_a?(String) && value.is_a?(String) } - render_api_error!('variables needs to be a map of key-valued strings', 400) - end - - project = find_project(params[:id]) - not_found! unless project - - result = Ci::PipelineTriggerService.new(project, nil, params).execute - not_found! unless result - - if result[:http_status] - render_api_error!(result[:message], result[:http_status]) - else - pipeline = result[:pipeline] - - # We switched to Ci::PipelineVariable from Ci::TriggerRequest.variables. - # Ci::TriggerRequest doesn't save variables anymore. - # Here is copying Ci::PipelineVariable to Ci::TriggerRequest.variables for presenting the variables. - # The same endpoint in v4 API pressents Pipeline instead of TriggerRequest, so it doesn't need such a process. - trigger_request = pipeline.trigger_requests.last - trigger_request.variables = params[:variables] - - present trigger_request, with: ::API::V3::Entities::TriggerRequest - end - end - - desc 'Get triggers list' do - success ::API::V3::Entities::Trigger - end - params do - use :pagination - end - get ':id/triggers' do - authenticate! - authorize! :admin_build, user_project - - triggers = user_project.triggers.includes(:trigger_requests) - - present paginate(triggers), with: ::API::V3::Entities::Trigger - end - - desc 'Get specific trigger of a project' do - success ::API::V3::Entities::Trigger - end - params do - requires :token, type: String, desc: 'The unique token of trigger' - end - get ':id/triggers/:token' do - authenticate! - authorize! :admin_build, user_project - - trigger = user_project.triggers.find_by(token: params[:token].to_s) - break not_found!('Trigger') unless trigger - - present trigger, with: ::API::V3::Entities::Trigger - end - - desc 'Create a trigger' do - success ::API::V3::Entities::Trigger - end - post ':id/triggers' do - authenticate! - authorize! :admin_build, user_project - - trigger = user_project.triggers.create - - present trigger, with: ::API::V3::Entities::Trigger - end - - desc 'Delete a trigger' do - success ::API::V3::Entities::Trigger - end - params do - requires :token, type: String, desc: 'The unique token of trigger' - end - delete ':id/triggers/:token' do - authenticate! - authorize! :admin_build, user_project - - trigger = user_project.triggers.find_by(token: params[:token].to_s) - break not_found!('Trigger') unless trigger - - trigger.destroy - - present trigger, with: ::API::V3::Entities::Trigger - end - end - end - end -end diff --git a/lib/api/v3/users.rb b/lib/api/v3/users.rb deleted file mode 100644 index cf106f2552d..00000000000 --- a/lib/api/v3/users.rb +++ /dev/null @@ -1,204 +0,0 @@ -module API - module V3 - class Users < Grape::API - include PaginationParams - include APIGuard - - allow_access_with_scope :read_user, if: -> (request) { request.get? } - - before do - authenticate! - end - - resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do - helpers do - params :optional_attributes do - optional :skype, type: String, desc: 'The Skype username' - optional :linkedin, type: String, desc: 'The LinkedIn username' - optional :twitter, type: String, desc: 'The Twitter username' - optional :website_url, type: String, desc: 'The website of the user' - optional :organization, type: String, desc: 'The organization of the user' - optional :projects_limit, type: Integer, desc: 'The number of projects a user can create' - optional :extern_uid, type: String, desc: 'The external authentication provider UID' - optional :provider, type: String, desc: 'The external provider' - optional :bio, type: String, desc: 'The biography of the user' - optional :location, type: String, desc: 'The location of the user' - optional :admin, type: Boolean, desc: 'Flag indicating the user is an administrator' - optional :can_create_group, type: Boolean, desc: 'Flag indicating the user can create groups' - optional :confirm, type: Boolean, default: true, desc: 'Flag indicating the account needs to be confirmed' - optional :external, type: Boolean, desc: 'Flag indicating the user is an external user' - all_or_none_of :extern_uid, :provider - end - end - - desc 'Create a user. Available only for admins.' do - success ::API::Entities::UserPublic - end - params do - requires :email, type: String, desc: 'The email of the user' - optional :password, type: String, desc: 'The password of the new user' - optional :reset_password, type: Boolean, desc: 'Flag indicating the user will be sent a password reset token' - at_least_one_of :password, :reset_password - requires :name, type: String, desc: 'The name of the user' - requires :username, type: String, desc: 'The username of the user' - use :optional_attributes - end - post do - authenticated_as_admin! - - params = declared_params(include_missing: false) - user = ::Users::CreateService.new(current_user, params.merge!(skip_confirmation: !params[:confirm])).execute - - if user.persisted? - present user, with: ::API::Entities::UserPublic - else - conflict!('Email has already been taken') if User - .where(email: user.email) - .count > 0 - - conflict!('Username has already been taken') if User - .where(username: user.username) - .count > 0 - - render_validation_error!(user) - end - end - - desc 'Get the SSH keys of a specified user. Available only for admins.' do - success ::API::Entities::SSHKey - end - params do - requires :id, type: Integer, desc: 'The ID of the user' - use :pagination - end - get ':id/keys' do - authenticated_as_admin! - - user = User.find_by(id: params[:id]) - not_found!('User') unless user - - present paginate(user.keys), with: ::API::Entities::SSHKey - end - - desc 'Get the emails addresses of a specified user. Available only for admins.' do - success ::API::Entities::Email - end - params do - requires :id, type: Integer, desc: 'The ID of the user' - use :pagination - end - get ':id/emails' do - authenticated_as_admin! - user = User.find_by(id: params[:id]) - not_found!('User') unless user - - present user.emails, with: ::API::Entities::Email - end - - desc 'Block a user. Available only for admins.' - params do - requires :id, type: Integer, desc: 'The ID of the user' - end - put ':id/block' do - authenticated_as_admin! - user = User.find_by(id: params[:id]) - not_found!('User') unless user - - if !user.ldap_blocked? - user.block - else - forbidden!('LDAP blocked users cannot be modified by the API') - end - end - - desc 'Unblock a user. Available only for admins.' - params do - requires :id, type: Integer, desc: 'The ID of the user' - end - put ':id/unblock' do - authenticated_as_admin! - user = User.find_by(id: params[:id]) - not_found!('User') unless user - - if user.ldap_blocked? - forbidden!('LDAP blocked users cannot be unblocked by the API') - else - user.activate - end - end - - desc 'Get the contribution events of a specified user' do - detail 'This feature was introduced in GitLab 8.13.' - success ::API::V3::Entities::Event - end - params do - requires :id, type: Integer, desc: 'The ID of the user' - use :pagination - end - get ':id/events' do - user = User.find_by(id: params[:id]) - not_found!('User') unless user - - events = user.events - .merge(ProjectsFinder.new(current_user: current_user).execute) - .references(:project) - .with_associations - .recent - - present paginate(events), with: ::API::V3::Entities::Event - end - - desc 'Delete an existing SSH key from a specified user. Available only for admins.' do - success ::API::Entities::SSHKey - end - params do - requires :id, type: Integer, desc: 'The ID of the user' - requires :key_id, type: Integer, desc: 'The ID of the SSH key' - end - delete ':id/keys/:key_id' do - authenticated_as_admin! - - user = User.find_by(id: params[:id]) - not_found!('User') unless user - - key = user.keys.find_by(id: params[:key_id]) - not_found!('Key') unless key - - present key.destroy, with: ::API::Entities::SSHKey - end - end - - resource :user do - desc "Get the currently authenticated user's SSH keys" do - success ::API::Entities::SSHKey - end - params do - use :pagination - end - get "keys" do - present current_user.keys, with: ::API::Entities::SSHKey - end - - desc "Get the currently authenticated user's email addresses" do - success ::API::Entities::Email - end - get "emails" do - present current_user.emails, with: ::API::Entities::Email - end - - desc 'Delete an SSH key from the currently authenticated user' do - success ::API::Entities::SSHKey - end - params do - requires :key_id, type: Integer, desc: 'The ID of the SSH key' - end - delete "keys/:key_id" do - key = current_user.keys.find_by(id: params[:key_id]) - not_found!('Key') unless key - - present key.destroy, with: ::API::Entities::SSHKey - end - end - end - end -end diff --git a/lib/api/v3/variables.rb b/lib/api/v3/variables.rb deleted file mode 100644 index 83972b1e7ce..00000000000 --- a/lib/api/v3/variables.rb +++ /dev/null @@ -1,29 +0,0 @@ -module API - module V3 - class Variables < Grape::API - include PaginationParams - - before { authenticate! } - before { authorize! :admin_build, user_project } - - params do - requires :id, type: String, desc: 'The ID of a project' - end - - resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Delete an existing variable from a project' do - success ::API::Entities::Variable - end - params do - requires :key, type: String, desc: 'The key of the variable' - end - delete ':id/variables/:key' do - variable = user_project.variables.find_by(key: params[:key]) - not_found!('Variable') unless variable - - present variable.destroy, with: ::API::Entities::Variable - end - end - end - end -end diff --git a/lib/bitbucket/representation/issue.rb b/lib/bitbucket/representation/issue.rb index 054064395c3..44bcbc250b3 100644 --- a/lib/bitbucket/representation/issue.rb +++ b/lib/bitbucket/representation/issue.rb @@ -12,7 +12,7 @@ module Bitbucket end def author - raw.fetch('reporter', {}).fetch('username', nil) + raw.dig('reporter', 'username') end def description diff --git a/lib/gitlab/auth/request_authenticator.rb b/lib/gitlab/auth/request_authenticator.rb index a0b5cd868c3..66de52506ce 100644 --- a/lib/gitlab/auth/request_authenticator.rb +++ b/lib/gitlab/auth/request_authenticator.rb @@ -16,7 +16,7 @@ module Gitlab end def find_sessionless_user - find_user_from_access_token || find_user_from_rss_token + find_user_from_access_token || find_user_from_feed_token rescue Gitlab::Auth::AuthenticationError nil end diff --git a/lib/gitlab/auth/user_auth_finders.rb b/lib/gitlab/auth/user_auth_finders.rb index 4dc23f977da..c7993665421 100644 --- a/lib/gitlab/auth/user_auth_finders.rb +++ b/lib/gitlab/auth/user_auth_finders.rb @@ -25,13 +25,15 @@ module Gitlab current_request.env['warden']&.authenticate if verified_request? end - def find_user_from_rss_token - return unless current_request.path.ends_with?('.atom') || current_request.format.atom? + def find_user_from_feed_token + return unless rss_request? || ics_request? - token = current_request.params[:rss_token].presence + # NOTE: feed_token was renamed from rss_token but both needs to be supported because + # users might have already added the feed to their RSS reader before the rename + token = current_request.params[:feed_token].presence || current_request.params[:rss_token].presence return unless token - User.find_by_rss_token(token) || raise(UnauthorizedError) + User.find_by_feed_token(token) || raise(UnauthorizedError) end def find_user_from_access_token @@ -104,6 +106,14 @@ module Gitlab def current_request @current_request ||= ensure_action_dispatch_request(request) end + + def rss_request? + current_request.path.ends_with?('.atom') || current_request.format.atom? + end + + def ics_request? + current_request.path.ends_with?('.ics') || current_request.format.ics? + end end end end diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb index d7369060cc5..4c28489f45a 100644 --- a/lib/gitlab/contributions_calendar.rb +++ b/lib/gitlab/contributions_calendar.rb @@ -85,7 +85,7 @@ module Gitlab .select(t[:project_id], t[:target_type], t[:action], "date(created_at + #{date_interval}) AS date", 'count(id) as total_amount') .group(t[:project_id], t[:target_type], t[:action], "date(created_at + #{date_interval})") .where(conditions) - .having(t[:project_id].in(Arel::Nodes::SqlLiteral.new(authed_projects.to_sql))) + .where("events.project_id in (#{authed_projects.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection end end end diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index 1f5f88bf792..a4cc64de80d 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -78,7 +78,7 @@ module Gitlab def tree_entry(ref, path, limit = nil) request = Gitaly::TreeEntryRequest.new( repository: @gitaly_repo, - revision: ref, + revision: encode_binary(ref), path: encode_binary(path), limit: limit.to_i ) diff --git a/lib/gitlab/gitlab_import/client.rb b/lib/gitlab/gitlab_import/client.rb index 5482504e72e..22719e9a003 100644 --- a/lib/gitlab/gitlab_import/client.rb +++ b/lib/gitlab/gitlab_import/client.rb @@ -29,28 +29,28 @@ module Gitlab end def user - api.get("/api/v3/user").parsed + api.get("/api/v4/user").parsed end def issues(project_identifier) lazy_page_iterator(PER_PAGE) do |page| - api.get("/api/v3/projects/#{project_identifier}/issues?per_page=#{PER_PAGE}&page=#{page}").parsed + api.get("/api/v4/projects/#{project_identifier}/issues?per_page=#{PER_PAGE}&page=#{page}").parsed end end def issue_comments(project_identifier, issue_id) lazy_page_iterator(PER_PAGE) do |page| - api.get("/api/v3/projects/#{project_identifier}/issues/#{issue_id}/notes?per_page=#{PER_PAGE}&page=#{page}").parsed + api.get("/api/v4/projects/#{project_identifier}/issues/#{issue_id}/notes?per_page=#{PER_PAGE}&page=#{page}").parsed end end def project(id) - api.get("/api/v3/projects/#{id}").parsed + api.get("/api/v4/projects/#{id}").parsed end def projects lazy_page_iterator(PER_PAGE) do |page| - api.get("/api/v3/projects?per_page=#{PER_PAGE}&page=#{page}").parsed + api.get("/api/v4/projects?per_page=#{PER_PAGE}&page=#{page}").parsed end end diff --git a/lib/gitlab/gitlab_import/importer.rb b/lib/gitlab/gitlab_import/importer.rb index e44d7934fda..195672f5a12 100644 --- a/lib/gitlab/gitlab_import/importer.rb +++ b/lib/gitlab/gitlab_import/importer.rb @@ -25,7 +25,7 @@ module Gitlab body = @formatter.author_line(issue["author"]["name"]) body += issue["description"] - comments = client.issue_comments(project_identifier, issue["id"]) + comments = client.issue_comments(project_identifier, issue["iid"]) if comments.any? body += @formatter.comments_header diff --git a/lib/gitlab/gitlab_import/project_creator.rb b/lib/gitlab/gitlab_import/project_creator.rb index 3d0418261bb..430b8c10058 100644 --- a/lib/gitlab/gitlab_import/project_creator.rb +++ b/lib/gitlab/gitlab_import/project_creator.rb @@ -17,7 +17,7 @@ module Gitlab path: repo["path"], description: repo["description"], namespace_id: namespace.id, - visibility_level: repo["visibility_level"], + visibility_level: Gitlab::VisibilityLevel.level_value(repo["visibility"]), import_type: "gitlab", import_source: repo["path_with_namespace"], import_url: repo["http_url_to_repo"].sub("://", "://oauth2:#{@session_data[:gitlab_access_token]}@") diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 0d31934347f..deaa14c8434 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -11,7 +11,7 @@ module Gitlab gon.asset_host = ActionController::Base.asset_host gon.webpack_public_path = webpack_public_path gon.relative_url_root = Gitlab.config.gitlab.relative_url_root - gon.shortcuts_path = help_page_path('shortcuts') + gon.shortcuts_path = Gitlab::Routing.url_helpers.help_page_path('shortcuts') gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class gon.sentry_dsn = Gitlab::CurrentSettings.clientside_sentry_dsn if Gitlab::CurrentSettings.clientside_sentry_enabled gon.gitlab_url = Gitlab.config.gitlab.url diff --git a/lib/gitlab/hashed_storage/rake_helper.rb b/lib/gitlab/hashed_storage/rake_helper.rb new file mode 100644 index 00000000000..8aba42ccfce --- /dev/null +++ b/lib/gitlab/hashed_storage/rake_helper.rb @@ -0,0 +1,71 @@ +module Gitlab + module HashedStorage + module RakeHelper + def self.batch_size + ENV.fetch('BATCH', 200).to_i + end + + def self.listing_limit + ENV.fetch('LIMIT', 500).to_i + end + + def self.project_id_batches(&block) + Project.with_unmigrated_storage.in_batches(of: batch_size, start: ENV['ID_FROM'], finish: ENV['ID_TO']) do |relation| # rubocop: disable Cop/InBatches + ids = relation.pluck(:id) + + yield ids.min, ids.max + end + end + + def self.legacy_attachments_relation + Upload.joins(<<~SQL).where('projects.storage_version < :version OR projects.storage_version IS NULL', version: Project::HASHED_STORAGE_FEATURES[:attachments]) + JOIN projects + ON (uploads.model_type='Project' AND uploads.model_id=projects.id) + SQL + end + + def self.hashed_attachments_relation + Upload.joins(<<~SQL).where('projects.storage_version >= :version', version: Project::HASHED_STORAGE_FEATURES[:attachments]) + JOIN projects + ON (uploads.model_type='Project' AND uploads.model_id=projects.id) + SQL + end + + def self.relation_summary(relation_name, relation) + relation_count = relation.count + $stdout.puts "* Found #{relation_count} #{relation_name}".color(:green) + + relation_count + end + + def self.projects_list(relation_name, relation) + listing(relation_name, relation.with_route) do |project| + $stdout.puts " - #{project.full_path} (id: #{project.id})".color(:red) + end + end + + def self.attachments_list(relation_name, relation) + listing(relation_name, relation) do |upload| + $stdout.puts " - #{upload.path} (id: #{upload.id})".color(:red) + end + end + + def self.listing(relation_name, relation) + relation_count = relation_summary(relation_name, relation) + return unless relation_count > 0 + + limit = listing_limit + + if relation_count > limit + $stdout.puts " ! Displaying first #{limit} #{relation_name}..." + end + + relation.find_each(batch_size: batch_size).with_index do |element, index| + yield element + + break if index + 1 >= limit + end + end + end + end +end diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 36c7534cd7a..da3667faf7a 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -28,6 +28,7 @@ project_tree: - project_members: - :user - merge_requests: + - :metrics - notes: - :author - events: diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index b736b2c3fe5..c5cf290f191 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -18,9 +18,10 @@ module Gitlab label: :project_label, custom_attributes: 'ProjectCustomAttribute', project_badges: 'Badge', + metrics: 'MergeRequest::Metrics', ci_cd_settings: 'ProjectCiCdSetting' }.freeze - USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id last_edited_by_id merge_user_id resolved_by_id closed_by_id].freeze + USER_REFERENCES = %w[author_id assignee_id updated_by_id merged_by_id latest_closed_by_id user_id created_by_id last_edited_by_id merge_user_id resolved_by_id closed_by_id].freeze PROJECT_REFERENCES = %w[project_id source_project_id target_project_id].freeze @@ -36,6 +37,15 @@ module Gitlab new(*args).create end + def self.relation_class(relation_name) + # There are scenarios where the model is pluralized (e.g. + # MergeRequest::Metrics), and we don't want to force it to singular + # with #classify. + relation_name.to_s.classify.constantize + rescue NameError + relation_name.to_s.constantize + end + def initialize(relation_sym:, relation_hash:, members_mapper:, user:, project:, excluded_keys: []) @relation_name = OVERRIDES[relation_sym] || relation_sym @relation_hash = relation_hash.except('noteable_id') @@ -195,7 +205,7 @@ module Gitlab end def relation_class - @relation_class ||= @relation_name.to_s.classify.constantize + @relation_class ||= self.class.relation_class(@relation_name) end def imported_object diff --git a/lib/gitlab/import_formatter.rb b/lib/gitlab/import_formatter.rb index 3e54456e936..4e611e7f16c 100644 --- a/lib/gitlab/import_formatter.rb +++ b/lib/gitlab/import_formatter.rb @@ -9,6 +9,7 @@ module Gitlab end def author_line(author) + author ||= "Anonymous" "*Created by: #{author}*\n\n" end end diff --git a/lib/gitlab/query_limiting/active_support_subscriber.rb b/lib/gitlab/query_limiting/active_support_subscriber.rb index 4c83581c4b1..3c4ff5d1928 100644 --- a/lib/gitlab/query_limiting/active_support_subscriber.rb +++ b/lib/gitlab/query_limiting/active_support_subscriber.rb @@ -4,7 +4,7 @@ module Gitlab attach_to :active_record def sql(event) - unless event.payload[:name] == 'CACHE' + unless event.payload.fetch(:cached, event.payload[:name] == 'CACHE') Transaction.current&.increment end end diff --git a/lib/gitlab/webpack/dev_server_middleware.rb b/lib/gitlab/webpack/dev_server_middleware.rb index b9a75eaac63..529f7d6a8d6 100644 --- a/lib/gitlab/webpack/dev_server_middleware.rb +++ b/lib/gitlab/webpack/dev_server_middleware.rb @@ -15,6 +15,11 @@ module Gitlab def perform_request(env) if @proxy_path && env['PATH_INFO'].start_with?("/#{@proxy_path}") + if relative_url_root = Rails.application.config.relative_url_root + env['SCRIPT_NAME'] = "" + env['REQUEST_PATH'].sub!(/\A#{Regexp.escape(relative_url_root)}/, '') + end + super(env) else @app.call(env) diff --git a/lib/mattermost/command.rb b/lib/mattermost/command.rb index 33e450d7f0a..704813dfdf0 100644 --- a/lib/mattermost/command.rb +++ b/lib/mattermost/command.rb @@ -1,7 +1,7 @@ module Mattermost class Command < Client def create(params) - response = session_post("/api/v3/teams/#{params[:team_id]}/commands/create", + response = session_post('/api/v4/commands', body: params.to_json) response['token'] diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb index 85f78e44f32..2aa7a2f64d8 100644 --- a/lib/mattermost/session.rb +++ b/lib/mattermost/session.rb @@ -112,7 +112,7 @@ module Mattermost end def destroy - post('/api/v3/users/logout') + post('/api/v4/users/logout') end def oauth_uri @@ -120,7 +120,7 @@ module Mattermost @oauth_uri = nil - response = get("/api/v3/oauth/gitlab/login", follow_redirects: false) + response = get('/oauth/gitlab/login', follow_redirects: false, format: 'text/html') return unless (300...400) === response.code redirect_uri = response.headers['location'] diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb index 75513a9ba04..95c2f6f9d6b 100644 --- a/lib/mattermost/team.rb +++ b/lib/mattermost/team.rb @@ -1,14 +1,14 @@ module Mattermost class Team < Client - # Returns **all** teams for an admin + # Returns all teams that the current user is a member of def all - session_get('/api/v3/teams/all').values + session_get("/api/v4/users/me/teams") end # Creates a team on the linked Mattermost instance, the team admin will be the # `current_user` passed to the Mattermost::Client instance def create(name:, display_name:, type:) - session_post('/api/v3/teams/create', body: { + session_post('/api/v4/teams', body: { name: name, display_name: display_name, type: type diff --git a/lib/peek/rblineprof/custom_controller_helpers.rb b/lib/peek/rblineprof/custom_controller_helpers.rb index 7cfe76b7b71..da24a36603e 100644 --- a/lib/peek/rblineprof/custom_controller_helpers.rb +++ b/lib/peek/rblineprof/custom_controller_helpers.rb @@ -41,10 +41,10 @@ module Peek ] end.sort_by{ |a,b,c,d,e,f| -f } - output = "<div class='modal-dialog modal-full'><div class='modal-content'>" + output = "<div class='modal-dialog modal-lg'><div class='modal-content'>" output << "<div class='modal-header'>" - output << "<button class='close btn btn-link btn-sm' type='button' data-dismiss='modal'>X</button>" output << "<h4>Line profiling: #{human_description(params[:lineprofiler])}</h4>" + output << "<button class='close' type='button' data-dismiss='modal' aria-label='close'><span aria-hidden='true'>×</span></button>" output << "</div>" output << "<div class='modal-body'>" diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab index 0e27a28ea6e..72eb8adcce2 100644 --- a/lib/support/nginx/gitlab +++ b/lib/support/nginx/gitlab @@ -31,27 +31,27 @@ map $http_upgrade $connection_upgrade_gitlab { log_format gitlab_access $remote_addr - $remote_user [$time_local] "$request_method $gitlab_filtered_request_uri $server_protocol" $status $body_bytes_sent "$gitlab_filtered_http_referer" "$http_user_agent"; ## Remove private_token from the request URI -# In: /foo?private_token=unfiltered&authenticity_token=unfiltered&rss_token=unfiltered&... -# Out: /foo?private_token=[FILTERED]&authenticity_token=unfiltered&rss_token=unfiltered&... +# In: /foo?private_token=unfiltered&authenticity_token=unfiltered&feed_token=unfiltered&... +# Out: /foo?private_token=[FILTERED]&authenticity_token=unfiltered&feed_token=unfiltered&... map $request_uri $gitlab_temp_request_uri_1 { default $request_uri; ~(?i)^(?<start>.*)(?<temp>[\?&]private[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest"; } ## Remove authenticity_token from the request URI -# In: /foo?private_token=[FILTERED]&authenticity_token=unfiltered&rss_token=unfiltered&... -# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=unfiltered&... +# In: /foo?private_token=[FILTERED]&authenticity_token=unfiltered&feed_token=unfiltered&... +# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&feed_token=unfiltered&... map $gitlab_temp_request_uri_1 $gitlab_temp_request_uri_2 { default $gitlab_temp_request_uri_1; ~(?i)^(?<start>.*)(?<temp>[\?&]authenticity[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest"; } -## Remove rss_token from the request URI -# In: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=unfiltered&... -# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=[FILTERED]&... +## Remove feed_token from the request URI +# In: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&feed_token=unfiltered&... +# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&feed_token=[FILTERED]&... map $gitlab_temp_request_uri_2 $gitlab_filtered_request_uri { default $gitlab_temp_request_uri_2; - ~(?i)^(?<start>.*)(?<temp>[\?&]rss[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest"; + ~(?i)^(?<start>.*)(?<temp>[\?&]feed[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest"; } ## A version of the referer without the query string diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index 8218d68f9ba..2e3799d5e1b 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -36,27 +36,27 @@ map $http_upgrade $connection_upgrade_gitlab_ssl { log_format gitlab_ssl_access $remote_addr - $remote_user [$time_local] "$request_method $gitlab_ssl_filtered_request_uri $server_protocol" $status $body_bytes_sent "$gitlab_ssl_filtered_http_referer" "$http_user_agent"; ## Remove private_token from the request URI -# In: /foo?private_token=unfiltered&authenticity_token=unfiltered&rss_token=unfiltered&... -# Out: /foo?private_token=[FILTERED]&authenticity_token=unfiltered&rss_token=unfiltered&... +# In: /foo?private_token=unfiltered&authenticity_token=unfiltered&feed_token=unfiltered&... +# Out: /foo?private_token=[FILTERED]&authenticity_token=unfiltered&feed_token=unfiltered&... map $request_uri $gitlab_ssl_temp_request_uri_1 { default $request_uri; ~(?i)^(?<start>.*)(?<temp>[\?&]private[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest"; } ## Remove authenticity_token from the request URI -# In: /foo?private_token=[FILTERED]&authenticity_token=unfiltered&rss_token=unfiltered&... -# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=unfiltered&... +# In: /foo?private_token=[FILTERED]&authenticity_token=unfiltered&feed_token=unfiltered&... +# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&feed_token=unfiltered&... map $gitlab_ssl_temp_request_uri_1 $gitlab_ssl_temp_request_uri_2 { default $gitlab_ssl_temp_request_uri_1; ~(?i)^(?<start>.*)(?<temp>[\?&]authenticity[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest"; } -## Remove rss_token from the request URI -# In: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=unfiltered&... -# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=[FILTERED]&... +## Remove feed_token from the request URI +# In: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&feed_token=unfiltered&... +# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&feed_token=[FILTERED]&... map $gitlab_ssl_temp_request_uri_2 $gitlab_ssl_filtered_request_uri { default $gitlab_ssl_temp_request_uri_2; - ~(?i)^(?<start>.*)(?<temp>[\?&]rss[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest"; + ~(?i)^(?<start>.*)(?<temp>[\?&]feed[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest"; } ## A version of the referer without the query string diff --git a/lib/tasks/gitlab/storage.rake b/lib/tasks/gitlab/storage.rake index 6e8bd9078c8..68d6f9d7cb1 100644 --- a/lib/tasks/gitlab/storage.rake +++ b/lib/tasks/gitlab/storage.rake @@ -3,6 +3,7 @@ namespace :gitlab do desc 'GitLab | Storage | Migrate existing projects to Hashed Storage' task migrate_to_hashed: :environment do legacy_projects_count = Project.with_unmigrated_storage.count + helper = Gitlab::HashedStorage::RakeHelper if legacy_projects_count == 0 puts 'There are no projects requiring storage migration. Nothing to do!' @@ -10,9 +11,9 @@ namespace :gitlab do next end - print "Enqueuing migration of #{legacy_projects_count} projects in batches of #{batch_size}" + print "Enqueuing migration of #{legacy_projects_count} projects in batches of #{helper.batch_size}" - project_id_batches do |start, finish| + helper.project_id_batches do |start, finish| StorageMigratorWorker.perform_async(start, finish) print '.' @@ -23,118 +24,50 @@ namespace :gitlab do desc 'Gitlab | Storage | Summary of existing projects using Legacy Storage' task legacy_projects: :environment do - relation_summary('projects', Project.without_storage_feature(:repository)) + helper = Gitlab::HashedStorage::RakeHelper + helper.relation_summary('projects using Legacy Storage', Project.without_storage_feature(:repository)) end desc 'Gitlab | Storage | List existing projects using Legacy Storage' task list_legacy_projects: :environment do - projects_list('projects using Legacy Storage', Project.without_storage_feature(:repository)) + helper = Gitlab::HashedStorage::RakeHelper + helper.projects_list('projects using Legacy Storage', Project.without_storage_feature(:repository)) end desc 'Gitlab | Storage | Summary of existing projects using Hashed Storage' task hashed_projects: :environment do - relation_summary('projects using Hashed Storage', Project.with_storage_feature(:repository)) + helper = Gitlab::HashedStorage::RakeHelper + helper.relation_summary('projects using Hashed Storage', Project.with_storage_feature(:repository)) end desc 'Gitlab | Storage | List existing projects using Hashed Storage' task list_hashed_projects: :environment do - projects_list('projects using Hashed Storage', Project.with_storage_feature(:repository)) + helper = Gitlab::HashedStorage::RakeHelper + helper.projects_list('projects using Hashed Storage', Project.with_storage_feature(:repository)) end desc 'Gitlab | Storage | Summary of project attachments using Legacy Storage' task legacy_attachments: :environment do - relation_summary('attachments using Legacy Storage', legacy_attachments_relation) + helper = Gitlab::HashedStorage::RakeHelper + helper.relation_summary('attachments using Legacy Storage', helper.legacy_attachments_relation) end desc 'Gitlab | Storage | List existing project attachments using Legacy Storage' task list_legacy_attachments: :environment do - attachments_list('attachments using Legacy Storage', legacy_attachments_relation) + helper = Gitlab::HashedStorage::RakeHelper + helper.attachments_list('attachments using Legacy Storage', helper.legacy_attachments_relation) end desc 'Gitlab | Storage | Summary of project attachments using Hashed Storage' task hashed_attachments: :environment do - relation_summary('attachments using Hashed Storage', hashed_attachments_relation) + helper = Gitlab::HashedStorage::RakeHelper + helper.relation_summary('attachments using Hashed Storage', helper.hashed_attachments_relation) end desc 'Gitlab | Storage | List existing project attachments using Hashed Storage' task list_hashed_attachments: :environment do - attachments_list('attachments using Hashed Storage', hashed_attachments_relation) - end - - def batch_size - ENV.fetch('BATCH', 200).to_i - end - - def project_id_batches(&block) - Project.with_unmigrated_storage.in_batches(of: batch_size, start: ENV['ID_FROM'], finish: ENV['ID_TO']) do |relation| # rubocop: disable Cop/InBatches - ids = relation.pluck(:id) - - yield ids.min, ids.max - end - end - - def legacy_attachments_relation - Upload.joins(<<~SQL).where('projects.storage_version < :version OR projects.storage_version IS NULL', version: Project::HASHED_STORAGE_FEATURES[:attachments]) - JOIN projects - ON (uploads.model_type='Project' AND uploads.model_id=projects.id) - SQL - end - - def hashed_attachments_relation - Upload.joins(<<~SQL).where('projects.storage_version >= :version', version: Project::HASHED_STORAGE_FEATURES[:attachments]) - JOIN projects - ON (uploads.model_type='Project' AND uploads.model_id=projects.id) - SQL - end - - def relation_summary(relation_name, relation) - relation_count = relation.count - puts "* Found #{relation_count} #{relation_name}".color(:green) - - relation_count - end - - def projects_list(relation_name, relation) - relation_count = relation_summary(relation_name, relation) - - projects = relation.with_route - limit = ENV.fetch('LIMIT', 500).to_i - - return unless relation_count > 0 - - puts " ! Displaying first #{limit} #{relation_name}..." if relation_count > limit - - counter = 0 - projects.find_in_batches(batch_size: batch_size) do |batch| - batch.each do |project| - counter += 1 - - puts " - #{project.full_path} (id: #{project.id})".color(:red) - - return if counter >= limit # rubocop:disable Lint/NonLocalExitFromIterator, Cop/AvoidReturnFromBlocks - end - end - end - - def attachments_list(relation_name, relation) - relation_count = relation_summary(relation_name, relation) - - limit = ENV.fetch('LIMIT', 500).to_i - - return unless relation_count > 0 - - puts " ! Displaying first #{limit} #{relation_name}..." if relation_count > limit - - counter = 0 - relation.find_in_batches(batch_size: batch_size) do |batch| - batch.each do |upload| - counter += 1 - - puts " - #{upload.path} (id: #{upload.id})".color(:red) - - return if counter >= limit # rubocop:disable Lint/NonLocalExitFromIterator, Cop/AvoidReturnFromBlocks - end - end + helper = Gitlab::HashedStorage::RakeHelper + helper.attachments_list('attachments using Hashed Storage', helper.hashed_attachments_relation) end end end diff --git a/lib/tasks/tokens.rake b/lib/tasks/tokens.rake index 693597afdf8..81829668de8 100644 --- a/lib/tasks/tokens.rake +++ b/lib/tasks/tokens.rake @@ -6,9 +6,9 @@ namespace :tokens do reset_all_users_token(:reset_incoming_email_token!) end - desc "Reset all GitLab RSS tokens" - task reset_all_rss: :environment do - reset_all_users_token(:reset_rss_token!) + desc "Reset all GitLab feed tokens" + task reset_all_feed: :environment do + reset_all_users_token(:reset_feed_token!) end def reset_all_users_token(reset_token_method) @@ -31,8 +31,8 @@ class TmpUser < ActiveRecord::Base save!(validate: false) end - def reset_rss_token! - write_new_token(:rss_token) + def reset_feed_token! + write_new_token(:feed_token) save!(validate: false) end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 9e34eb463ce..035a2275d9f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-05-23 07:40-0500\n" -"PO-Revision-Date: 2018-05-23 07:40-0500\n" +"POT-Creation-Date: 2018-05-29 09:43-0500\n" +"PO-Revision-Date: 2018-05-29 09:43-0500\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -178,6 +178,21 @@ msgstr "" msgid "2FA enabled" msgstr "" +msgid "403|Please contact your GitLab administrator to get the permission." +msgstr "" + +msgid "403|You don't have the permission to access this page." +msgstr "" + +msgid "404|Make sure the address is correct and the page hasn't moved." +msgstr "" + +msgid "404|Page Not Found" +msgstr "" + +msgid "404|Please contact your GitLab administrator if you think this is a mistake." +msgstr "" + msgid "<strong>Removes</strong> source branch" msgstr "" @@ -301,6 +316,9 @@ msgstr "" msgid "AdminUsers|To confirm, type %{username}" msgstr "" +msgid "Advanced" +msgstr "" + msgid "Advanced settings" msgstr "" @@ -484,7 +502,7 @@ msgstr "" msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly." msgstr "" -msgid "AutoDevOps|Auto DevOps (Beta)" +msgid "AutoDevOps|Auto DevOps" msgstr "" msgid "AutoDevOps|Auto DevOps documentation" @@ -505,7 +523,7 @@ msgstr "" msgid "AutoDevOps|add a Kubernetes cluster" msgstr "" -msgid "AutoDevOps|enable Auto DevOps (Beta)" +msgid "AutoDevOps|enable Auto DevOps" msgstr "" msgid "Available" @@ -768,7 +786,7 @@ msgstr "" msgid "CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery." msgstr "" -msgid "CICD|Auto DevOps (Beta)" +msgid "CICD|Auto DevOps" msgstr "" msgid "CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration." @@ -987,6 +1005,12 @@ msgstr "" msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration" msgstr "" +msgid "ClusterIntegration|An error occured while trying to fetch project zones: %{error}" +msgstr "" + +msgid "ClusterIntegration|An error occured while trying to fetch your projects: %{error}" +msgstr "" + msgid "ClusterIntegration|Applications" msgstr "" @@ -1047,6 +1071,15 @@ msgstr "" msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for new GCP accounts to get started with GitLab's Google Kubernetes Engine Integration." msgstr "" +msgid "ClusterIntegration|Fetching machine types" +msgstr "" + +msgid "ClusterIntegration|Fetching projects" +msgstr "" + +msgid "ClusterIntegration|Fetching zones" +msgstr "" + msgid "ClusterIntegration|GitLab Integration" msgstr "" @@ -1143,6 +1176,18 @@ msgstr "" msgid "ClusterIntegration|More information" msgstr "" +msgid "ClusterIntegration|No machine types matched your search" +msgstr "" + +msgid "ClusterIntegration|No projects found" +msgstr "" + +msgid "ClusterIntegration|No projects matched your search" +msgstr "" + +msgid "ClusterIntegration|No zones matched your search" +msgstr "" + msgid "ClusterIntegration|Note:" msgstr "" @@ -1155,9 +1200,6 @@ msgstr "" msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:" msgstr "" -msgid "ClusterIntegration|Project ID" -msgstr "" - msgid "ClusterIntegration|Project namespace" msgstr "" @@ -1188,19 +1230,40 @@ msgstr "" msgid "ClusterIntegration|Save changes" msgstr "" +msgid "ClusterIntegration|Search machine types" +msgstr "" + +msgid "ClusterIntegration|Search projects" +msgstr "" + +msgid "ClusterIntegration|Search zones" +msgstr "" + msgid "ClusterIntegration|Security" msgstr "" msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster" msgstr "" -msgid "ClusterIntegration|See machine types" +msgid "ClusterIntegration|See zones" msgstr "" -msgid "ClusterIntegration|See your projects" +msgid "ClusterIntegration|Select machine type" msgstr "" -msgid "ClusterIntegration|See zones" +msgid "ClusterIntegration|Select project" +msgstr "" + +msgid "ClusterIntegration|Select project and zone to choose machine type" +msgstr "" + +msgid "ClusterIntegration|Select project to choose zone" +msgstr "" + +msgid "ClusterIntegration|Select zone" +msgstr "" + +msgid "ClusterIntegration|Select zone to choose machine type" msgstr "" msgid "ClusterIntegration|Service token" @@ -1233,6 +1296,9 @@ msgstr "" msgid "ClusterIntegration|Token" msgstr "" +msgid "ClusterIntegration|Validating project billing status" +msgstr "" + msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way." msgstr "" @@ -1866,6 +1932,9 @@ msgstr "" msgid "Enable group Runners" msgstr "" +msgid "Enable or disable certain group features and choose access levels." +msgstr "" + msgid "Enable or disable version check and usage ping." msgstr "" @@ -1953,6 +2022,15 @@ msgstr "" msgid "Error fetching usage ping data." msgstr "" +msgid "Error loading branch data. Please try again." +msgstr "" + +msgid "Error loading last commit." +msgstr "" + +msgid "Error loading project data. Please try again." +msgstr "" + msgid "Error occurred when toggling the notification subscription" msgstr "" @@ -2129,6 +2207,9 @@ msgstr "" msgid "Gitaly Servers" msgstr "" +msgid "Go Back" +msgstr "" + msgid "Go back" msgstr "" @@ -2631,6 +2712,18 @@ msgstr "" msgid "Name new label" msgstr "" +msgid "Nav|Help" +msgstr "" + +msgid "Nav|Home" +msgstr "" + +msgid "Nav|Sign In / Register" +msgstr "" + +msgid "Nav|Sign out and sign in with a different account" +msgstr "" + msgid "New Issue" msgid_plural "New Issues" msgstr[0] "" @@ -2885,12 +2978,18 @@ msgstr "" msgid "Pending" msgstr "" +msgid "Perform advanced options such as changing path, transferring, or removing the group." +msgstr "" + msgid "Performance optimization" msgstr "" msgid "Permalink" msgstr "" +msgid "Permissions" +msgstr "" + msgid "Personal Access Token" msgstr "" @@ -3050,9 +3149,6 @@ msgstr "" msgid "Play" msgstr "" -msgid "Please <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again." -msgstr "" - msgid "Please accept the Terms of Service before continuing." msgstr "" @@ -3502,6 +3598,9 @@ msgstr "" msgid "Search files" msgstr "" +msgid "Search for projects, issues, etc." +msgstr "" + msgid "Search milestones" msgstr "" @@ -3517,7 +3616,7 @@ msgstr "" msgid "Seconds to wait for a storage access attempt" msgstr "" -msgid "Variables" +msgid "Select" msgstr "" msgid "Select Archive Format" @@ -3538,6 +3637,15 @@ msgstr "" msgid "Select branch/tag" msgstr "" +msgid "Select project" +msgstr "" + +msgid "Select project and zone to choose machine type" +msgstr "" + +msgid "Select project to choose zone" +msgstr "" + msgid "Select source branch" msgstr "" @@ -3636,6 +3744,9 @@ msgstr "" msgid "Something went wrong when toggling the button" msgstr "" +msgid "Something went wrong while fetching the latest pipeline status." +msgstr "" + msgid "Something went wrong while fetching the projects." msgstr "" @@ -4359,6 +4470,9 @@ msgstr "" msgid "Up to date" msgstr "" +msgid "Update your group name, description, avatar, and other general settings." +msgstr "" + msgid "Upload New File" msgstr "" @@ -4389,6 +4503,9 @@ msgstr "" msgid "User and IP Rate Limits" msgstr "" +msgid "Variables" +msgstr "" + msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want." msgstr "" @@ -4440,9 +4557,6 @@ msgstr "" msgid "Want to see the data? Please ask an administrator for access." msgstr "" -msgid "We could not verify that one of your projects on GCP has billing enabled. Please try again." -msgstr "" - msgid "We don't have enough data to show this stage." msgstr "" diff --git a/package.json b/package.json index defcbd34d95..7be3b8744e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "clean": "rm -rf public/assets tmp/cache/*-loader", "dev-server": "nodemon -w 'config/webpack.config.js' --exec 'webpack-dev-server --config config/webpack.config.js'", "eslint": "eslint --max-warnings 0 --ext .js,.vue .", "eslint-fix": "eslint --max-warnings 0 --ext .js,.vue --fix .", @@ -100,21 +101,21 @@ }, "devDependencies": { "axios-mock-adapter": "^1.15.0", - "babel-eslint": "^8.0.2", + "babel-eslint": "^8.2.3", "babel-plugin-istanbul": "^4.1.6", "babel-plugin-rewire": "^1.1.0", "babel-template": "^6.26.0", "babel-types": "^6.26.0", "chalk": "^2.4.1", "commander": "^2.15.1", - "eslint": "^3.18.0", - "eslint-config-airbnb-base": "^10.0.1", - "eslint-import-resolver-webpack": "^0.8.3", - "eslint-plugin-filenames": "^1.1.0", - "eslint-plugin-html": "2.0.1", - "eslint-plugin-import": "^2.2.0", + "eslint": "~4.12.1", + "eslint-config-airbnb-base": "^12.1.0", + "eslint-import-resolver-webpack": "^0.10.0", + "eslint-plugin-filenames": "^1.2.0", + "eslint-plugin-html": "4.0.3", + "eslint-plugin-import": "^2.12.0", "eslint-plugin-jasmine": "^2.1.0", - "eslint-plugin-promise": "^3.5.0", + "eslint-plugin-promise": "^3.8.0", "eslint-plugin-vue": "^4.0.1", "ignore": "^3.3.7", "istanbul": "^0.4.5", diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb index 166861e6c4a..9507f92f4b2 100644 --- a/qa/qa/page/merge_request/show.rb +++ b/qa/qa/page/merge_request/show.rb @@ -16,6 +16,10 @@ module QA element :no_fast_forward_message, 'Fast-forward merge is not possible' end + view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue' do + element :squash_checkbox + end + def rebase! click_element :mr_rebase_button @@ -41,6 +45,14 @@ module QA has_text?('The changes were merged into') end end + + def mark_to_squash + wait(reload: true) do + has_css?(element_selector_css(:squash_checkbox)) + end + + click_element :squash_checkbox + end end end end diff --git a/qa/qa/page/project/settings/merge_request.rb b/qa/qa/page/project/settings/merge_request.rb index b147c91b467..a88cd661016 100644 --- a/qa/qa/page/project/settings/merge_request.rb +++ b/qa/qa/page/project/settings/merge_request.rb @@ -5,7 +5,7 @@ module QA class MergeRequest < QA::Page::Base include Common - view 'app/views/projects/_merge_request_fast_forward_settings.html.haml' do + view 'app/views/projects/_merge_request_merge_method_settings.html.haml' do element :radio_button_merge_ff end diff --git a/qa/qa/specs/features/merge_request/squash_spec.rb b/qa/qa/specs/features/merge_request/squash_spec.rb new file mode 100644 index 00000000000..dbbdf852a38 --- /dev/null +++ b/qa/qa/specs/features/merge_request/squash_spec.rb @@ -0,0 +1,48 @@ +module QA + feature 'merge request squash commits', :core do + scenario 'when squash commits is marked before merge' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + project = Factory::Resource::Project.fabricate! do |project| + project.name = "squash-before-merge" + end + + merge_request = Factory::Resource::MergeRequest.fabricate! do |merge_request| + merge_request.project = project + merge_request.title = 'Squashing commits' + end + + Factory::Repository::Push.fabricate! do |push| + push.project = project + push.commit_message = 'to be squashed' + push.branch_name = merge_request.source_branch + push.new_branch = false + push.file_name = 'other.txt' + push.file_content = "Test with unicode characters ❤✓€❄" + end + + merge_request.visit! + + Page::MergeRequest::Show.perform do |merge_request_page| + merge_request_page.mark_to_squash + merge_request_page.merge! + + merge_request.project.visit! + + Git::Repository.perform do |repository| + repository.uri = Page::Project::Show.act do + choose_repository_clone_http + repository_location.uri + end + + repository.use_default_credentials + + repository.act { clone } + + expect(repository.commits.size).to eq 3 + end + end + end + end +end diff --git a/qa/spec/runtime/api_request_spec.rb b/qa/spec/runtime/api_request_spec.rb index 9a1ed8a7a46..8cf4b040c24 100644 --- a/qa/spec/runtime/api_request_spec.rb +++ b/qa/spec/runtime/api_request_spec.rb @@ -36,7 +36,7 @@ describe QA::Runtime::API::Request do end it 'uses a different api version' do - expect(request.request_path('/users', version: 'v3')).to eq '/api/v3/users' + expect(request.request_path('/users', version: 'other_version')).to eq '/api/other_version/users' end end end diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index f0caac40afd..b048da1991c 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -146,35 +146,43 @@ describe ApplicationController do end end - describe '#authenticate_user_from_rss_token' do - describe "authenticating a user from an RSS token" do + describe '#authenticate_sessionless_user!' do + describe 'authenticating a user from a feed token' do controller(described_class) do def index render text: 'authenticated' end end - context "when the 'rss_token' param is populated with the RSS token" do + context "when the 'feed_token' param is populated with the feed token" do context 'when the request format is atom' do it "logs the user in" do - get :index, rss_token: user.rss_token, format: :atom + get :index, feed_token: user.feed_token, format: :atom expect(response).to have_gitlab_http_status 200 expect(response.body).to eq 'authenticated' end end - context 'when the request format is not atom' do + context 'when the request format is ics' do + it "logs the user in" do + get :index, feed_token: user.feed_token, format: :ics + expect(response).to have_gitlab_http_status 200 + expect(response.body).to eq 'authenticated' + end + end + + context 'when the request format is neither atom nor ics' do it "doesn't log the user in" do - get :index, rss_token: user.rss_token + get :index, feed_token: user.feed_token expect(response.status).not_to have_gitlab_http_status 200 expect(response.body).not_to eq 'authenticated' end end end - context "when the 'rss_token' param is populated with an invalid RSS token" do + context "when the 'feed_token' param is populated with an invalid feed token" do it "doesn't log the user" do - get :index, rss_token: "token" + get :index, feed_token: 'token', format: :atom expect(response.status).not_to eq 200 expect(response.body).not_to eq 'authenticated' end @@ -454,7 +462,7 @@ describe ApplicationController do end it 'renders a 403 when the sessionless user did not accept the terms' do - get :index, rss_token: user.rss_token, format: :atom + get :index, feed_token: user.feed_token, format: :atom expect(response).to have_gitlab_http_status(403) end @@ -462,7 +470,7 @@ describe ApplicationController do it 'renders a 200 when the sessionless user accepted the terms' do accept_terms(user) - get :index, rss_token: user.rss_token, format: :atom + get :index, feed_token: user.feed_token, format: :atom expect(response).to have_gitlab_http_status(200) end diff --git a/spec/controllers/groups/shared_projects_controller_spec.rb b/spec/controllers/groups/shared_projects_controller_spec.rb new file mode 100644 index 00000000000..d8fa41abb18 --- /dev/null +++ b/spec/controllers/groups/shared_projects_controller_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe Groups::SharedProjectsController do + def get_shared_projects(params = {}) + get :index, params.reverse_merge(format: :json, group_id: group.full_path) + end + + def share_project(project) + Projects::GroupLinks::CreateService.new( + project, + user, + link_group_access: ProjectGroupLink::DEVELOPER + ).execute(group) + end + + set(:group) { create(:group) } + set(:user) { create(:user) } + set(:shared_project) do + shared_project = create(:project, namespace: user.namespace) + share_project(shared_project) + + shared_project + end + + let(:json_project_ids) { json_response.map { |project_info| project_info['id'] } } + + before do + sign_in(user) + end + + describe 'GET #index' do + it 'returns only projects shared with the group' do + create(:project, namespace: group) + + get_shared_projects + + expect(json_project_ids).to contain_exactly(shared_project.id) + end + + it 'allows filtering shared projects' do + project = create(:project, :archived, namespace: user.namespace, name: "Searching for") + share_project(project) + + get_shared_projects(filter: 'search') + + expect(json_project_ids).to contain_exactly(project.id) + end + + it 'allows sorting projects' do + shared_project.update!(name: 'bbb') + second_project = create(:project, namespace: user.namespace, name: 'aaaa') + share_project(second_project) + + get_shared_projects(sort: 'name_asc') + + expect(json_project_ids).to eq([second_project.id, shared_project.id]) + end + end +end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index d3042be9e8b..6e8de6db9c3 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -275,6 +275,7 @@ describe Projects::MergeRequestsController do namespace_id: project.namespace, project_id: project, id: merge_request.iid, + squash: false, format: 'json' } end @@ -315,8 +316,8 @@ describe Projects::MergeRequestsController do end context 'when the sha parameter matches the source SHA' do - def merge_with_sha - post :merge, base_params.merge(sha: merge_request.diff_head_sha) + def merge_with_sha(params = {}) + post :merge, base_params.merge(sha: merge_request.diff_head_sha).merge(params) end it 'returns :success' do @@ -325,12 +326,30 @@ describe Projects::MergeRequestsController do expect(json_response).to eq('status' => 'success') end - it 'starts the merge immediately' do - expect(MergeWorker).to receive(:perform_async).with(merge_request.id, anything, anything) + it 'starts the merge immediately with permitted params' do + expect(MergeWorker).to receive(:perform_async).with(merge_request.id, anything, { 'squash' => false }) merge_with_sha end + context 'when squash is passed as 1' do + it 'updates the squash attribute on the MR to true' do + merge_request.update(squash: false) + merge_with_sha(squash: '1') + + expect(merge_request.reload.squash).to be_truthy + end + end + + context 'when squash is passed as 0' do + it 'updates the squash attribute on the MR to false' do + merge_request.update(squash: true) + merge_with_sha(squash: '0') + + expect(merge_request.reload.squash).to be_falsey + end + end + context 'when the pipeline succeeds is passed' do let!(:head_pipeline) do create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch, head_pipeline_of: merge_request) diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 769fd656e7a..59db8cdc34b 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -12,10 +12,6 @@ FactoryBot.define do user.notification_email = user.email end - before(:create) do |user| - user.ensure_rss_token - end - trait :admin do admin true end diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb index fb6c71ce997..da7749b42d2 100644 --- a/spec/features/atom/dashboard_issues_spec.rb +++ b/spec/features/atom/dashboard_issues_spec.rb @@ -31,20 +31,20 @@ describe "Dashboard Issues Feed" do expect(body).to have_selector('title', text: "#{user.name} issues") end - it "renders atom feed via RSS token" do - visit issues_dashboard_path(:atom, rss_token: user.rss_token, assignee_id: user.id) + it "renders atom feed via feed token" do + visit issues_dashboard_path(:atom, feed_token: user.feed_token, assignee_id: user.id) expect(response_headers['Content-Type']).to have_content('application/atom+xml') expect(body).to have_selector('title', text: "#{user.name} issues") end it "renders atom feed with url parameters" do - visit issues_dashboard_path(:atom, rss_token: user.rss_token, state: 'opened', assignee_id: user.id) + visit issues_dashboard_path(:atom, feed_token: user.feed_token, state: 'opened', assignee_id: user.id) link = find('link[type="application/atom+xml"]') params = CGI.parse(URI.parse(link[:href]).query) - expect(params).to include('rss_token' => [user.rss_token]) + expect(params).to include('feed_token' => [user.feed_token]) expect(params).to include('state' => ['opened']) expect(params).to include('assignee_id' => [user.id.to_s]) end @@ -53,7 +53,7 @@ describe "Dashboard Issues Feed" do let!(:issue2) { create(:issue, author: user, assignees: [assignee], project: project2, description: 'test desc') } it "renders issue fields" do - visit issues_dashboard_path(:atom, rss_token: user.rss_token, assignee_id: assignee.id) + visit issues_dashboard_path(:atom, feed_token: user.feed_token, assignee_id: assignee.id) entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue2.title}')]") @@ -76,7 +76,7 @@ describe "Dashboard Issues Feed" do end it "renders issue label and milestone info" do - visit issues_dashboard_path(:atom, rss_token: user.rss_token, assignee_id: assignee.id) + visit issues_dashboard_path(:atom, feed_token: user.feed_token, assignee_id: assignee.id) entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue1.title}')]") diff --git a/spec/features/atom/dashboard_spec.rb b/spec/features/atom/dashboard_spec.rb index c6683bb3bc9..462eab07a75 100644 --- a/spec/features/atom/dashboard_spec.rb +++ b/spec/features/atom/dashboard_spec.rb @@ -13,9 +13,9 @@ describe "Dashboard Feed" do end end - context "projects atom feed via RSS token" do + context "projects atom feed via feed token" do it "renders projects atom feed" do - visit dashboard_projects_path(:atom, rss_token: user.rss_token) + visit dashboard_projects_path(:atom, feed_token: user.feed_token) expect(body).to have_selector('feed title') end end @@ -29,7 +29,7 @@ describe "Dashboard Feed" do project.add_master(user) issue_event(issue, user) note_event(note, user) - visit dashboard_projects_path(:atom, rss_token: user.rss_token) + visit dashboard_projects_path(:atom, feed_token: user.feed_token) end it "has issue opened event" do diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb index 525ce23aa56..ee3570a5b2b 100644 --- a/spec/features/atom/issues_spec.rb +++ b/spec/features/atom/issues_spec.rb @@ -45,10 +45,10 @@ describe 'Issues Feed' do end end - context 'when authenticated via RSS token' do + context 'when authenticated via feed token' do it 'renders atom feed' do visit project_issues_path(project, :atom, - rss_token: user.rss_token) + feed_token: user.feed_token) expect(response_headers['Content-Type']) .to have_content('application/atom+xml') @@ -61,24 +61,23 @@ describe 'Issues Feed' do end it "renders atom feed with url parameters for project issues" do - visit project_issues_path(project, - :atom, rss_token: user.rss_token, state: 'opened', assignee_id: user.id) + visit project_issues_path(project, :atom, feed_token: user.feed_token, state: 'opened', assignee_id: user.id) link = find('link[type="application/atom+xml"]') params = CGI.parse(URI.parse(link[:href]).query) - expect(params).to include('rss_token' => [user.rss_token]) + expect(params).to include('feed_token' => [user.feed_token]) expect(params).to include('state' => ['opened']) expect(params).to include('assignee_id' => [user.id.to_s]) end it "renders atom feed with url parameters for group issues" do - visit issues_group_path(group, :atom, rss_token: user.rss_token, state: 'opened', assignee_id: user.id) + visit issues_group_path(group, :atom, feed_token: user.feed_token, state: 'opened', assignee_id: user.id) link = find('link[type="application/atom+xml"]') params = CGI.parse(URI.parse(link[:href]).query) - expect(params).to include('rss_token' => [user.rss_token]) + expect(params).to include('feed_token' => [user.feed_token]) expect(params).to include('state' => ['opened']) expect(params).to include('assignee_id' => [user.id.to_s]) end diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb index 2d074c115dd..eeaaa40fe21 100644 --- a/spec/features/atom/users_spec.rb +++ b/spec/features/atom/users_spec.rb @@ -13,9 +13,9 @@ describe "User Feed" do end end - context 'user atom feed via RSS token' do + context 'user atom feed via feed token' do it "renders user atom feed" do - visit user_path(user, :atom, rss_token: user.rss_token) + visit user_path(user, :atom, feed_token: user.feed_token) expect(body).to have_selector('feed title') end end @@ -51,7 +51,7 @@ describe "User Feed" do issue_event(issue, user) note_event(note, user) merge_request_event(merge_request, user) - visit user_path(user, :atom, rss_token: user.rss_token) + visit user_path(user, :atom, feed_token: user.feed_token) end it 'has issue opened event' do diff --git a/spec/features/dashboard/activity_spec.rb b/spec/features/dashboard/activity_spec.rb index a74a8aac2b2..941208fa244 100644 --- a/spec/features/dashboard/activity_spec.rb +++ b/spec/features/dashboard/activity_spec.rb @@ -12,8 +12,8 @@ feature 'Dashboard > Activity' do visit activity_dashboard_path end - it_behaves_like "it has an RSS button with current_user's RSS token" - it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token" + it_behaves_like "it has an RSS button with current_user's feed token" + it_behaves_like "an autodiscoverable RSS feed with current_user's feed token" end context 'event filters', :js do diff --git a/spec/features/dashboard/issues_filter_spec.rb b/spec/features/dashboard/issues_filter_spec.rb index bab34ac9346..8d0b0be1bd4 100644 --- a/spec/features/dashboard/issues_filter_spec.rb +++ b/spec/features/dashboard/issues_filter_spec.rb @@ -47,15 +47,15 @@ feature 'Dashboard Issues filtering', :js do it 'updates atom feed link' do visit_issues(milestone_title: '', assignee_id: user.id) - link = find('.nav-controls a[title="Subscribe"]') + link = find('.nav-controls a[title="Subscribe to RSS feed"]') params = CGI.parse(URI.parse(link[:href]).query) auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query) - expect(params).to include('rss_token' => [user.rss_token]) + expect(params).to include('feed_token' => [user.feed_token]) expect(params).to include('milestone_title' => ['']) expect(params).to include('assignee_id' => [user.id.to_s]) - expect(auto_discovery_params).to include('rss_token' => [user.rss_token]) + expect(auto_discovery_params).to include('feed_token' => [user.feed_token]) expect(auto_discovery_params).to include('milestone_title' => ['']) expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s]) end diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb index e41a2e4ce09..3cc7b38550d 100644 --- a/spec/features/dashboard/issues_spec.rb +++ b/spec/features/dashboard/issues_spec.rb @@ -56,8 +56,8 @@ RSpec.describe 'Dashboard Issues' do expect(page).to have_current_path(issues_dashboard_url(assignee_id: current_user.id, state: 'closed'), url: true) end - it_behaves_like "it has an RSS button with current_user's RSS token" - it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token" + it_behaves_like "it has an RSS button with current_user's feed token" + it_behaves_like "an autodiscoverable RSS feed with current_user's feed token" end describe 'new issue dropdown' do diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb index 257a3822503..ef2f0b5b31a 100644 --- a/spec/features/dashboard/projects_spec.rb +++ b/spec/features/dashboard/projects_spec.rb @@ -10,7 +10,7 @@ feature 'Dashboard Projects' do sign_in(user) end - it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token" do + it_behaves_like "an autodiscoverable RSS feed with current_user's feed token" do before do visit dashboard_projects_path end diff --git a/spec/features/error_pages_spec.rb b/spec/features/error_pages_spec.rb new file mode 100644 index 00000000000..cd7bcf29cc9 --- /dev/null +++ b/spec/features/error_pages_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe 'Error Pages' do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + + before do + sign_in(user) + end + + shared_examples 'shows nav links' do + it 'shows nav links' do + expect(page).to have_link("Home", href: root_path) + expect(page).to have_link("Help", href: help_path) + expect(page).to have_link(nil, href: destroy_user_session_path) + end + end + + describe '404' do + before do + visit '/not-a-real-page' + end + + it 'allows user to search' do + fill_in 'search', with: 'something' + click_button 'Search' + + expect(page).to have_current_path(%r{^/search\?.*search=something.*}) + end + + it_behaves_like 'shows nav links' + end + + describe '403' do + before do + visit '/' + visit edit_project_path(project) + end + + it_behaves_like 'shows nav links' + end +end diff --git a/spec/features/groups/activity_spec.rb b/spec/features/groups/activity_spec.rb index 7bc809b3104..0d7d3771071 100644 --- a/spec/features/groups/activity_spec.rb +++ b/spec/features/groups/activity_spec.rb @@ -15,8 +15,8 @@ feature 'Group activity page' do visit path end - it_behaves_like "it has an RSS button with current_user's RSS token" - it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token" + it_behaves_like "it has an RSS button with current_user's feed token" + it_behaves_like "an autodiscoverable RSS feed with current_user's feed token" end context 'when project is in the group', :js do @@ -39,7 +39,7 @@ feature 'Group activity page' do visit path end - it_behaves_like "it has an RSS button without an RSS token" - it_behaves_like "an autodiscoverable RSS feed without an RSS token" + it_behaves_like "it has an RSS button without a feed token" + it_behaves_like "an autodiscoverable RSS feed without a feed token" end end diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb index 90bf7ba49f6..111a24c0d94 100644 --- a/spec/features/groups/issues_spec.rb +++ b/spec/features/groups/issues_spec.rb @@ -16,17 +16,21 @@ feature 'Group issues page' do let(:access_level) { ProjectFeature::ENABLED } context 'when signed in' do - let(:user) { user_in_group } - - it_behaves_like "it has an RSS button with current_user's RSS token" - it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token" + let(:user) do + user_in_group.ensure_feed_token + user_in_group.save! + user_in_group + end + + it_behaves_like "it has an RSS button with current_user's feed token" + it_behaves_like "an autodiscoverable RSS feed with current_user's feed token" end context 'when signed out' do let(:user) { nil } - it_behaves_like "it has an RSS button without an RSS token" - it_behaves_like "an autodiscoverable RSS feed without an RSS token" + it_behaves_like "it has an RSS button without a feed token" + it_behaves_like "an autodiscoverable RSS feed without a feed token" end end diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb index 3a0424d60f8..b7a7aa0e174 100644 --- a/spec/features/groups/show_spec.rb +++ b/spec/features/groups/show_spec.rb @@ -14,7 +14,7 @@ feature 'Group show page' do visit path end - it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token" + it_behaves_like "an autodiscoverable RSS feed with current_user's feed token" context 'when group does not exist' do let(:path) { group_path('not-exist') } @@ -29,7 +29,7 @@ feature 'Group show page' do visit path end - it_behaves_like "an autodiscoverable RSS feed without an RSS token" + it_behaves_like "an autodiscoverable RSS feed without a feed token" end context 'when group has a public project', :js do diff --git a/spec/features/ics/dashboard_issues_spec.rb b/spec/features/ics/dashboard_issues_spec.rb new file mode 100644 index 00000000000..5d6cd44ad1c --- /dev/null +++ b/spec/features/ics/dashboard_issues_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe 'Dashboard Issues Calendar Feed' do + describe 'GET /issues' do + let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') } + let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') } + let!(:project) { create(:project) } + + before do + project.add_master(user) + end + + context 'when authenticated' do + it 'renders calendar feed' do + sign_in user + visit issues_dashboard_path(:ics) + + expect(response_headers['Content-Type']).to have_content('text/calendar') + expect(response_headers['Content-Disposition']).to have_content('inline') + expect(body).to have_text('BEGIN:VCALENDAR') + end + end + + context 'when authenticated via personal access token' do + it 'renders calendar feed' do + personal_access_token = create(:personal_access_token, user: user) + + visit issues_dashboard_path(:ics, private_token: personal_access_token.token) + + expect(response_headers['Content-Type']).to have_content('text/calendar') + expect(response_headers['Content-Disposition']).to have_content('inline') + expect(body).to have_text('BEGIN:VCALENDAR') + end + end + + context 'when authenticated via feed token' do + it 'renders calendar feed' do + visit issues_dashboard_path(:ics, feed_token: user.feed_token) + + expect(response_headers['Content-Type']).to have_content('text/calendar') + expect(response_headers['Content-Disposition']).to have_content('inline') + expect(body).to have_text('BEGIN:VCALENDAR') + end + end + + context 'issue with due date' do + let!(:issue) do + create(:issue, author: user, assignees: [assignee], project: project, title: 'test title', + description: 'test desc', due_date: Date.tomorrow) + end + + it 'renders issue fields' do + visit issues_dashboard_path(:ics, feed_token: user.feed_token) + + expect(body).to have_text("SUMMARY:test title (in #{project.full_path})") + # line length for ics is 75 chars + expected_description = "DESCRIPTION:Find out more at #{issue_url(issue)}".insert(75, "\r\n") + expect(body).to have_text(expected_description) + expect(body).to have_text("DTSTART;VALUE=DATE:#{Date.tomorrow.strftime('%Y%m%d')}") + expect(body).to have_text("URL:#{issue_url(issue)}") + expect(body).to have_text('TRANSP:TRANSPARENT') + end + end + end +end diff --git a/spec/features/ics/group_issues_spec.rb b/spec/features/ics/group_issues_spec.rb new file mode 100644 index 00000000000..0a049be2ffe --- /dev/null +++ b/spec/features/ics/group_issues_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +describe 'Group Issues Calendar Feed' do + describe 'GET /issues' do + let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') } + let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') } + let!(:group) { create(:group) } + let!(:project) { create(:project, group: group) } + + before do + project.add_developer(user) + group.add_developer(user) + end + + context 'when authenticated' do + it 'renders calendar feed' do + sign_in user + visit issues_group_path(group, :ics) + + expect(response_headers['Content-Type']).to have_content('text/calendar') + expect(response_headers['Content-Disposition']).to have_content('inline') + expect(body).to have_text('BEGIN:VCALENDAR') + end + end + + context 'when authenticated via personal access token' do + it 'renders calendar feed' do + personal_access_token = create(:personal_access_token, user: user) + + visit issues_group_path(group, :ics, private_token: personal_access_token.token) + + expect(response_headers['Content-Type']).to have_content('text/calendar') + expect(response_headers['Content-Disposition']).to have_content('inline') + expect(body).to have_text('BEGIN:VCALENDAR') + end + end + + context 'when authenticated via feed token' do + it 'renders calendar feed' do + visit issues_group_path(group, :ics, feed_token: user.feed_token) + + expect(response_headers['Content-Type']).to have_content('text/calendar') + expect(response_headers['Content-Disposition']).to have_content('inline') + expect(body).to have_text('BEGIN:VCALENDAR') + end + end + + context 'issue with due date' do + let!(:issue) do + create(:issue, author: user, assignees: [assignee], project: project, title: 'test title', + description: 'test desc', due_date: Date.tomorrow) + end + + it 'renders issue fields' do + visit issues_group_path(group, :ics, feed_token: user.feed_token) + + expect(body).to have_text("SUMMARY:test title (in #{project.full_path})") + # line length for ics is 75 chars + expected_description = "DESCRIPTION:Find out more at #{issue_url(issue)}".insert(75, "\r\n") + expect(body).to have_text(expected_description) + expect(body).to have_text("DTSTART;VALUE=DATE:#{Date.tomorrow.strftime('%Y%m%d')}") + expect(body).to have_text("URL:#{issue_url(issue)}") + expect(body).to have_text('TRANSP:TRANSPARENT') + end + end + end +end diff --git a/spec/features/ics/project_issues_spec.rb b/spec/features/ics/project_issues_spec.rb new file mode 100644 index 00000000000..b99e9607f1d --- /dev/null +++ b/spec/features/ics/project_issues_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +describe 'Project Issues Calendar Feed' do + describe 'GET /issues' do + let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') } + let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') } + let!(:project) { create(:project) } + let!(:issue) { create(:issue, author: user, assignees: [assignee], project: project) } + + before do + project.add_developer(user) + end + + context 'when authenticated' do + it 'renders calendar feed' do + sign_in user + visit project_issues_path(project, :ics) + + expect(response_headers['Content-Type']).to have_content('text/calendar') + expect(response_headers['Content-Disposition']).to have_content('inline') + expect(body).to have_text('BEGIN:VCALENDAR') + end + end + + context 'when authenticated via personal access token' do + it 'renders calendar feed' do + personal_access_token = create(:personal_access_token, user: user) + + visit project_issues_path(project, :ics, private_token: personal_access_token.token) + + expect(response_headers['Content-Type']).to have_content('text/calendar') + expect(response_headers['Content-Disposition']).to have_content('inline') + expect(body).to have_text('BEGIN:VCALENDAR') + end + end + + context 'when authenticated via feed token' do + it 'renders calendar feed' do + visit project_issues_path(project, :ics, feed_token: user.feed_token) + + expect(response_headers['Content-Type']).to have_content('text/calendar') + expect(response_headers['Content-Disposition']).to have_content('inline') + expect(body).to have_text('BEGIN:VCALENDAR') + end + end + + context 'issue with due date' do + let!(:issue) do + create(:issue, author: user, assignees: [assignee], project: project, title: 'test title', + description: 'test desc', due_date: Date.tomorrow) + end + + it 'renders issue fields' do + visit project_issues_path(project, :ics, feed_token: user.feed_token) + + expect(body).to have_text("SUMMARY:test title (in #{project.full_path})") + # line length for ics is 75 chars + expected_description = "DESCRIPTION:Find out more at #{issue_url(issue)}".insert(75, "\r\n") + expect(body).to have_text(expected_description) + expect(body).to have_text("DTSTART;VALUE=DATE:#{Date.tomorrow.strftime('%Y%m%d')}") + expect(body).to have_text("URL:#{issue_url(issue)}") + expect(body).to have_text('TRANSP:TRANSPARENT') + end + end + end +end diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index 483122ae463..bc42618306f 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -468,13 +468,13 @@ describe 'Filter issues', :js do it "for #{type}" do visit path - link = find_link('Subscribe') + link = find_link('Subscribe to RSS feed') params = CGI.parse(URI.parse(link[:href]).query) auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query) expected = { - 'rss_token' => [user.rss_token], + 'feed_token' => [user.feed_token], 'milestone_title' => [milestone.title], 'assignee_id' => [user.id.to_s] } diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 314bd19f586..c6dcd97631d 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -340,6 +340,20 @@ describe 'Issues' do expect(page).to have_content('baz') end end + + it 'filters by due next month and previous two weeks' do + foo.update(due_date: Date.today - 4.weeks) + bar.update(due_date: (Date.today + 2.months).beginning_of_month) + baz.update(due_date: Date.yesterday) + + visit project_issues_path(project, due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name) + + page.within '.issues-holder' do + expect(page).not_to have_content('foo') + expect(page).not_to have_content('bar') + expect(page).to have_content('baz') + end + end end describe 'sorting by milestone' do @@ -591,6 +605,20 @@ describe 'Issues' do end end + it 'clears local storage after creating a new issue', :js do + 2.times do + visit new_project_issue_path(project) + wait_for_requests + + expect(page).to have_field('Title', with: '') + + fill_in 'issue_title', with: 'bug 345' + fill_in 'issue_description', with: 'bug description' + + click_button 'Submit issue' + end + end + context 'dropzone upload file', :js do before do visit new_project_issue_path(project) diff --git a/spec/features/merge_requests/user_squashes_merge_request_spec.rb b/spec/features/merge_requests/user_squashes_merge_request_spec.rb new file mode 100644 index 00000000000..6c952791591 --- /dev/null +++ b/spec/features/merge_requests/user_squashes_merge_request_spec.rb @@ -0,0 +1,124 @@ +require 'spec_helper' + +feature 'User squashes a merge request', :js do + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:source_branch) { 'csv' } + + let!(:original_head) { project.repository.commit('master') } + + shared_examples 'squash' do + it 'squashes the commits into a single commit, and adds a merge commit' do + expect(page).to have_content('Merged') + + latest_master_commits = project.repository.commits_between(original_head.sha, 'master').map(&:raw) + + squash_commit = an_object_having_attributes(sha: a_string_matching(/\h{40}/), + message: "Csv\n", + author_name: user.name, + committer_name: user.name) + + merge_commit = an_object_having_attributes(sha: a_string_matching(/\h{40}/), + message: a_string_starting_with("Merge branch 'csv' into 'master'"), + author_name: user.name, + committer_name: user.name) + + expect(project.repository).not_to be_merged_to_root_ref(source_branch) + expect(latest_master_commits).to match([squash_commit, merge_commit]) + end + end + + shared_examples 'no squash' do + it 'accepts the merge request without squashing' do + expect(page).to have_content('Merged') + expect(project.repository).to be_merged_to_root_ref(source_branch) + end + end + + def accept_mr + expect(page).to have_button('Merge') + + uncheck 'Remove source branch' + click_on 'Merge' + end + + before do + # Prevent source branch from being removed so we can use be_merged_to_root_ref + # method to check if squash was performed or not + allow_any_instance_of(MergeRequest).to receive(:force_remove_source_branch?).and_return(false) + project.add_master(user) + + sign_in user + end + + context 'when the MR has only one commit' do + before do + merge_request = create(:merge_request, source_project: project, target_project: project, source_branch: 'master', target_branch: 'branch-merged') + + visit project_merge_request_path(project, merge_request) + end + + it 'does not show the squash checkbox' do + expect(page).not_to have_field('squash') + end + end + + context 'when squash is enabled on merge request creation' do + before do + visit project_new_merge_request_path(project, merge_request: { target_branch: 'master', source_branch: source_branch }) + check 'merge_request[squash]' + click_on 'Submit merge request' + wait_for_requests + end + + it 'shows the squash checkbox as checked' do + expect(page).to have_checked_field('squash') + end + + context 'when accepting with squash checked' do + before do + accept_mr + end + + include_examples 'squash' + end + + context 'when accepting and unchecking squash' do + before do + uncheck 'squash' + accept_mr + end + + include_examples 'no squash' + end + end + + context 'when squash is not enabled on merge request creation' do + before do + visit project_new_merge_request_path(project, merge_request: { target_branch: 'master', source_branch: source_branch }) + click_on 'Submit merge request' + wait_for_requests + end + + it 'shows the squash checkbox as unchecked' do + expect(page).to have_unchecked_field('squash') + end + + context 'when accepting and checking squash' do + before do + check 'squash' + accept_mr + end + + include_examples 'squash' + end + + context 'when accepting with squash unchecked' do + before do + accept_mr + end + + include_examples 'no squash' + end + end +end diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb index 15dcb30cbdd..2e0753c3bfb 100644 --- a/spec/features/profile_spec.rb +++ b/spec/features/profile_spec.rb @@ -56,21 +56,21 @@ describe 'Profile account page', :js do end end - describe 'when I reset RSS token' do + describe 'when I reset feed token' do before do visit profile_personal_access_tokens_path end - it 'resets RSS token' do - within('.rss-token-reset') do - previous_token = find("#rss_token").value + it 'resets feed token' do + within('.feed-token-reset') do + previous_token = find("#feed_token").value accept_confirm { click_link('reset it') } - expect(find('#rss_token').value).not_to eq(previous_token) + expect(find('#feed_token').value).not_to eq(previous_token) end - expect(page).to have_content 'RSS token was successfully reset' + expect(page).to have_content 'Feed token was successfully reset' end end diff --git a/spec/features/projects/activity/rss_spec.rb b/spec/features/projects/activity/rss_spec.rb index cd1cfe07998..4ac34adde0e 100644 --- a/spec/features/projects/activity/rss_spec.rb +++ b/spec/features/projects/activity/rss_spec.rb @@ -15,7 +15,7 @@ feature 'Project Activity RSS' do visit path end - it_behaves_like "it has an RSS button with current_user's RSS token" + it_behaves_like "it has an RSS button with current_user's feed token" end context 'when signed out' do @@ -23,6 +23,6 @@ feature 'Project Activity RSS' do visit path end - it_behaves_like "it has an RSS button without an RSS token" + it_behaves_like "it has an RSS button without a feed token" end end diff --git a/spec/features/projects/commits/rss_spec.rb b/spec/features/projects/commits/rss_spec.rb index 0d9c7355ddd..0bc207da970 100644 --- a/spec/features/projects/commits/rss_spec.rb +++ b/spec/features/projects/commits/rss_spec.rb @@ -12,8 +12,8 @@ feature 'Project Commits RSS' do visit path end - it_behaves_like "it has an RSS button with current_user's RSS token" - it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token" + it_behaves_like "it has an RSS button with current_user's feed token" + it_behaves_like "an autodiscoverable RSS feed with current_user's feed token" end context 'when signed out' do @@ -21,7 +21,7 @@ feature 'Project Commits RSS' do visit path end - it_behaves_like "it has an RSS button without an RSS token" - it_behaves_like "an autodiscoverable RSS feed without an RSS token" + it_behaves_like "it has an RSS button without a feed token" + it_behaves_like "an autodiscoverable RSS feed without a feed token" end end diff --git a/spec/features/projects/issues/rss_spec.rb b/spec/features/projects/issues/rss_spec.rb index ff91aabc311..8b1f7d432ee 100644 --- a/spec/features/projects/issues/rss_spec.rb +++ b/spec/features/projects/issues/rss_spec.rb @@ -17,8 +17,8 @@ feature 'Project Issues RSS' do visit path end - it_behaves_like "it has an RSS button with current_user's RSS token" - it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token" + it_behaves_like "it has an RSS button with current_user's feed token" + it_behaves_like "an autodiscoverable RSS feed with current_user's feed token" end context 'when signed out' do @@ -26,7 +26,7 @@ feature 'Project Issues RSS' do visit path end - it_behaves_like "it has an RSS button without an RSS token" - it_behaves_like "an autodiscoverable RSS feed without an RSS token" + it_behaves_like "it has an RSS button without a feed token" + it_behaves_like "an autodiscoverable RSS feed without a feed token" end end diff --git a/spec/features/projects/milestones/milestone_spec.rb b/spec/features/projects/milestones/milestone_spec.rb index 30de3e83fbb..20a52d6011f 100644 --- a/spec/features/projects/milestones/milestone_spec.rb +++ b/spec/features/projects/milestones/milestone_spec.rb @@ -17,8 +17,8 @@ feature 'Project milestone' do it 'shows issues tab' do within('#content-body') do expect(page).to have_link 'Issues', href: '#tab-issues' - expect(page).to have_selector '.nav-links li.active', count: 1 - expect(find('.nav-links li.active')).to have_content 'Issues' + expect(page).to have_selector '.nav-links li a.active', count: 1 + expect(find('.nav-links li a.active')).to have_content 'Issues' end end @@ -44,8 +44,8 @@ feature 'Project milestone' do it 'hides issues tab' do within('#content-body') do expect(page).not_to have_link 'Issues', href: '#tab-issues' - expect(page).to have_selector '.nav-links li.active', count: 1 - expect(find('.nav-links li.active')).to have_content 'Merge Requests' + expect(page).to have_selector '.nav-links li a.active', count: 1 + expect(find('.nav-links li a.active')).to have_content 'Merge Requests' end end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index af2a9567a47..35776a5f23b 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -379,7 +379,7 @@ describe 'Pipeline', :js do end it 'fails to access the page' do - expect(page).to have_content('Access Denied') + expect(page).to have_title('Access Denied') end end end diff --git a/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb b/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb index b2906e315f7..fce41ce347f 100644 --- a/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb +++ b/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb @@ -64,7 +64,7 @@ feature 'Setup Mattermost slash commands', :js do click_link 'Add to Mattermost' expect(page).to have_content('The team where the slash commands will be used in') - expect(page).to have_content('This is the only available team.') + expect(page).to have_content('This is the only available team that you are a member of.') end it 'shows a disabled prefilled select if user is a member of 1 team' do @@ -94,7 +94,7 @@ feature 'Setup Mattermost slash commands', :js do click_link 'Add to Mattermost' expect(page).to have_content('Select the team where the slash commands will be used in') - expect(page).to have_content('The list shows all available teams.') + expect(page).to have_content('The list shows all available teams that you are a member of.') end it 'shows a select with team options user is a member of multiple teams' do diff --git a/spec/features/projects/show/rss_spec.rb b/spec/features/projects/show/rss_spec.rb index d02eaf34533..52164d30c40 100644 --- a/spec/features/projects/show/rss_spec.rb +++ b/spec/features/projects/show/rss_spec.rb @@ -12,7 +12,7 @@ feature 'Projects > Show > RSS' do visit path end - it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token" + it_behaves_like "an autodiscoverable RSS feed with current_user's feed token" end context 'when signed out' do @@ -20,6 +20,6 @@ feature 'Projects > Show > RSS' do visit path end - it_behaves_like "an autodiscoverable RSS feed without an RSS token" + it_behaves_like "an autodiscoverable RSS feed without a feed token" end end diff --git a/spec/features/projects/tree/rss_spec.rb b/spec/features/projects/tree/rss_spec.rb index 6407370ac0d..f52b3cc1d86 100644 --- a/spec/features/projects/tree/rss_spec.rb +++ b/spec/features/projects/tree/rss_spec.rb @@ -12,7 +12,7 @@ feature 'Project Tree RSS' do visit path end - it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token" + it_behaves_like "an autodiscoverable RSS feed with current_user's feed token" end context 'when signed out' do @@ -20,6 +20,6 @@ feature 'Project Tree RSS' do visit path end - it_behaves_like "an autodiscoverable RSS feed without an RSS token" + it_behaves_like "an autodiscoverable RSS feed without a feed token" end end diff --git a/spec/features/users/rss_spec.rb b/spec/features/users/rss_spec.rb index 7c5abe54d56..c3734b5c808 100644 --- a/spec/features/users/rss_spec.rb +++ b/spec/features/users/rss_spec.rb @@ -10,7 +10,7 @@ feature 'User RSS' do visit path end - it_behaves_like "it has an RSS button with current_user's RSS token" + it_behaves_like "it has an RSS button with current_user's feed token" end context 'when signed out' do @@ -18,6 +18,6 @@ feature 'User RSS' do visit path end - it_behaves_like "it has an RSS button without an RSS token" + it_behaves_like "it has an RSS button without a feed token" end end diff --git a/spec/features/users/terms_spec.rb b/spec/features/users/terms_spec.rb index f9469adbfe3..1efa5cd5490 100644 --- a/spec/features/users/terms_spec.rb +++ b/spec/features/users/terms_spec.rb @@ -62,7 +62,8 @@ describe 'Users > Terms' do expect(current_path).to eq(project_issues_path(project)) end - it 'redirects back to the page the user was trying to save' do + # Disabled until https://gitlab.com/gitlab-org/gitlab-ce/issues/37162 is solved properly + xit 'redirects back to the page the user was trying to save' do visit new_project_issue_path(project) fill_in :issue_title, with: 'Hello world, a new issue' diff --git a/spec/fixtures/api/schemas/entities/merge_request_widget.json b/spec/fixtures/api/schemas/entities/merge_request_widget.json index 233102c4314..7be8c9e3e67 100644 --- a/spec/fixtures/api/schemas/entities/merge_request_widget.json +++ b/spec/fixtures/api/schemas/entities/merge_request_widget.json @@ -112,7 +112,8 @@ "rebase_commit_sha": { "type": ["string", "null"] }, "rebase_in_progress": { "type": "boolean" }, "can_push_to_source_branch": { "type": "boolean" }, - "rebase_path": { "type": ["string", "null"] } + "rebase_path": { "type": ["string", "null"] }, + "squash": { "type": "boolean" } }, "additionalProperties": false } diff --git a/spec/fixtures/api/schemas/public_api/v3/issues.json b/spec/fixtures/api/schemas/public_api/v3/issues.json deleted file mode 100644 index 51b0822bc66..00000000000 --- a/spec/fixtures/api/schemas/public_api/v3/issues.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "type": "array", - "items": { - "type": "object", - "properties" : { - "id": { "type": "integer" }, - "iid": { "type": "integer" }, - "project_id": { "type": "integer" }, - "title": { "type": "string" }, - "description": { "type": ["string", "null"] }, - "state": { "type": "string" }, - "created_at": { "type": "date" }, - "updated_at": { "type": "date" }, - "labels": { - "type": "array", - "items": { - "type": "string" - } - }, - "milestone": { - "type": "object", - "properties": { - "id": { "type": "integer" }, - "iid": { "type": "integer" }, - "project_id": { "type": ["integer", "null"] }, - "group_id": { "type": ["integer", "null"] }, - "title": { "type": "string" }, - "description": { "type": ["string", "null"] }, - "state": { "type": "string" }, - "created_at": { "type": "date" }, - "updated_at": { "type": "date" }, - "due_date": { "type": "date" }, - "start_date": { "type": "date" } - }, - "additionalProperties": false - }, - "assignee": { - "type": ["object", "null"], - "properties": { - "name": { "type": "string" }, - "username": { "type": "string" }, - "id": { "type": "integer" }, - "state": { "type": "string" }, - "avatar_url": { "type": "uri" }, - "web_url": { "type": "uri" } - }, - "additionalProperties": false - }, - "author": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "username": { "type": "string" }, - "id": { "type": "integer" }, - "state": { "type": "string" }, - "avatar_url": { "type": "uri" }, - "web_url": { "type": "uri" } - }, - "additionalProperties": false - }, - "user_notes_count": { "type": "integer" }, - "upvotes": { "type": "integer" }, - "downvotes": { "type": "integer" }, - "due_date": { "type": ["date", "null"] }, - "confidential": { "type": "boolean" }, - "web_url": { "type": "uri" }, - "subscribed": { "type": ["boolean"] } - }, - "required": [ - "id", "iid", "project_id", "title", "description", - "state", "created_at", "updated_at", "labels", - "milestone", "assignee", "author", "user_notes_count", - "upvotes", "downvotes", "due_date", "confidential", - "web_url", "subscribed" - ], - "additionalProperties": false - } -} diff --git a/spec/fixtures/api/schemas/public_api/v3/merge_requests.json b/spec/fixtures/api/schemas/public_api/v3/merge_requests.json deleted file mode 100644 index b5c74bcc26e..00000000000 --- a/spec/fixtures/api/schemas/public_api/v3/merge_requests.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "type": "array", - "items": { - "type": "object", - "properties" : { - "id": { "type": "integer" }, - "iid": { "type": "integer" }, - "project_id": { "type": "integer" }, - "title": { "type": "string" }, - "description": { "type": ["string", "null"] }, - "state": { "type": "string" }, - "created_at": { "type": "date" }, - "updated_at": { "type": "date" }, - "target_branch": { "type": "string" }, - "source_branch": { "type": "string" }, - "upvotes": { "type": "integer" }, - "downvotes": { "type": "integer" }, - "author": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "username": { "type": "string" }, - "id": { "type": "integer" }, - "state": { "type": "string" }, - "avatar_url": { "type": "uri" }, - "web_url": { "type": "uri" } - }, - "additionalProperties": false - }, - "assignee": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "username": { "type": "string" }, - "id": { "type": "integer" }, - "state": { "type": "string" }, - "avatar_url": { "type": "uri" }, - "web_url": { "type": "uri" } - }, - "additionalProperties": false - }, - "source_project_id": { "type": "integer" }, - "target_project_id": { "type": "integer" }, - "labels": { - "type": "array", - "items": { - "type": "string" - } - }, - "work_in_progress": { "type": "boolean" }, - "milestone": { - "type": ["object", "null"], - "properties": { - "id": { "type": "integer" }, - "iid": { "type": "integer" }, - "project_id": { "type": ["integer", "null"] }, - "group_id": { "type": ["integer", "null"] }, - "title": { "type": "string" }, - "description": { "type": ["string", "null"] }, - "state": { "type": "string" }, - "created_at": { "type": "date" }, - "updated_at": { "type": "date" }, - "due_date": { "type": "date" }, - "start_date": { "type": "date" } - }, - "additionalProperties": false - }, - "merge_when_build_succeeds": { "type": "boolean" }, - "merge_status": { "type": "string" }, - "sha": { "type": "string" }, - "merge_commit_sha": { "type": ["string", "null"] }, - "user_notes_count": { "type": "integer" }, - "should_remove_source_branch": { "type": ["boolean", "null"] }, - "force_remove_source_branch": { "type": ["boolean", "null"] }, - "web_url": { "type": "uri" }, - "subscribed": { "type": ["boolean"] } - }, - "required": [ - "id", "iid", "project_id", "title", "description", - "state", "created_at", "updated_at", "target_branch", - "source_branch", "upvotes", "downvotes", "author", - "assignee", "source_project_id", "target_project_id", - "labels", "work_in_progress", "milestone", "merge_when_build_succeeds", - "merge_status", "sha", "merge_commit_sha", "user_notes_count", - "should_remove_source_branch", "force_remove_source_branch", - "web_url", "subscribed" - ], - "additionalProperties": false - } -} diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json index 0dc2eabec5d..f97461ce9cc 100644 --- a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json +++ b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json @@ -75,6 +75,7 @@ "force_remove_source_branch": { "type": ["boolean", "null"] }, "discussion_locked": { "type": ["boolean", "null"] }, "web_url": { "type": "uri" }, + "squash": { "type": "boolean" }, "time_stats": { "time_estimate": { "type": "integer" }, "total_time_spent": { "type": "integer" }, @@ -91,7 +92,7 @@ "labels", "work_in_progress", "milestone", "merge_when_pipeline_succeeds", "merge_status", "sha", "merge_commit_sha", "user_notes_count", "should_remove_source_branch", "force_remove_source_branch", - "web_url" + "web_url", "squash" ], "additionalProperties": false } diff --git a/spec/helpers/calendar_helper_spec.rb b/spec/helpers/calendar_helper_spec.rb new file mode 100644 index 00000000000..828a9d9fea0 --- /dev/null +++ b/spec/helpers/calendar_helper_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe CalendarHelper do + describe '#calendar_url_options' do + context 'when signed in' do + it "includes the current_user's feed_token" do + current_user = create(:user) + allow(helper).to receive(:current_user).and_return(current_user) + expect(helper.calendar_url_options).to include feed_token: current_user.feed_token + end + end + + context 'when signed out' do + it "does not have a feed_token" do + allow(helper).to receive(:current_user).and_return(nil) + expect(helper.calendar_url_options[:feed_token]).to be_nil + end + end + end +end diff --git a/spec/helpers/rss_helper_spec.rb b/spec/helpers/rss_helper_spec.rb index 269e1057e8d..a7f9bdf07e4 100644 --- a/spec/helpers/rss_helper_spec.rb +++ b/spec/helpers/rss_helper_spec.rb @@ -3,17 +3,17 @@ require 'spec_helper' describe RssHelper do describe '#rss_url_options' do context 'when signed in' do - it "includes the current_user's rss_token" do + it "includes the current_user's feed_token" do current_user = create(:user) allow(helper).to receive(:current_user).and_return(current_user) - expect(helper.rss_url_options).to include rss_token: current_user.rss_token + expect(helper.rss_url_options).to include feed_token: current_user.feed_token end end context 'when signed out' do - it "does not have an rss_token" do + it "does not have a feed_token" do allow(helper).to receive(:current_user).and_return(nil) - expect(helper.rss_url_options[:rss_token]).to be_nil + expect(helper.rss_url_options[:feed_token]).to be_nil end end end diff --git a/spec/initializers/grape_route_helpers_fix_spec.rb b/spec/initializers/grape_route_helpers_fix_spec.rb deleted file mode 100644 index 2cf5924128f..00000000000 --- a/spec/initializers/grape_route_helpers_fix_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'spec_helper' -require_relative '../../config/initializers/grape_route_helpers_fix' - -describe 'route shadowing' do - include GrapeRouteHelpers::NamedRouteMatcher - - it 'does not occur' do - path = api_v4_projects_merge_requests_path(id: 1) - expect(path).to eq('/api/v4/projects/1/merge_requests') - - path = api_v4_projects_merge_requests_path(id: 1, merge_request_iid: 3) - expect(path).to eq('/api/v4/projects/1/merge_requests/3') - end -end diff --git a/spec/javascripts/.eslintrc b/spec/javascripts/.eslintrc deleted file mode 100644 index 9eb0e732572..00000000000 --- a/spec/javascripts/.eslintrc +++ /dev/null @@ -1,33 +0,0 @@ -{ - "env": { - "jasmine": true - }, - "extends": "plugin:jasmine/recommended", - "globals": { - "appendLoadFixtures": false, - "appendLoadStyleFixtures": false, - "appendSetFixtures": false, - "appendSetStyleFixtures": false, - "getJSONFixture": false, - "loadFixtures": false, - "loadJSONFixtures": false, - "loadStyleFixtures": false, - "preloadFixtures": false, - "preloadStyleFixtures": false, - "readFixtures": false, - "sandbox": false, - "setFixtures": false, - "setStyleFixtures": false, - "spyOnDependency": false, - "spyOnEvent": false, - "ClassSpecHelper": false - }, - "plugins": ["jasmine"], - "rules": { - "func-names": 0, - "jasmine/no-suite-dupes": [1, "branch"], - "jasmine/no-spec-dupes": [1, "branch"], - "no-console": 0, - "prefer-arrow-callback": 0 - } -} diff --git a/spec/javascripts/.eslintrc.yml b/spec/javascripts/.eslintrc.yml new file mode 100644 index 00000000000..8bceb2c50fc --- /dev/null +++ b/spec/javascripts/.eslintrc.yml @@ -0,0 +1,34 @@ +--- +env: + jasmine: true +extends: plugin:jasmine/recommended +globals: + appendLoadFixtures: false + appendLoadStyleFixtures: false + appendSetFixtures: false + appendSetStyleFixtures: false + getJSONFixture: false + loadFixtures: false + loadJSONFixtures: false + loadStyleFixtures: false + preloadFixtures: false + preloadStyleFixtures: false + readFixtures: false + sandbox: false + setFixtures: false + setStyleFixtures: false + spyOnDependency: false + spyOnEvent: false + ClassSpecHelper: false +plugins: + - jasmine +rules: + func-names: off + jasmine/no-suite-dupes: + - warn + - branch + jasmine/no-spec-dupes: + - warn + - branch + no-console: off + prefer-arrow-callback: off diff --git a/spec/javascripts/blob/notebook/index_spec.js b/spec/javascripts/blob/notebook/index_spec.js index a143fc827d5..80c09a544d6 100644 --- a/spec/javascripts/blob/notebook/index_spec.js +++ b/spec/javascripts/blob/notebook/index_spec.js @@ -84,9 +84,14 @@ describe('iPython notebook renderer', () => { describe('error in JSON response', () => { let mock; - beforeEach((done) => { + beforeEach(done => { mock = new MockAdapter(axios); - mock.onGet('/test').reply(() => Promise.reject({ status: 200, data: '{ "cells": [{"cell_type": "markdown"} }' })); + mock + .onGet('/test') + .reply(() => + // eslint-disable-next-line prefer-promise-reject-errors + Promise.reject({ status: 200, data: '{ "cells": [{"cell_type": "markdown"} }' }), + ); renderNotebook(); diff --git a/spec/javascripts/boards/board_blank_state_spec.js b/spec/javascripts/boards/board_blank_state_spec.js index 664ea202e93..89a4fae4b59 100644 --- a/spec/javascripts/boards/board_blank_state_spec.js +++ b/spec/javascripts/boards/board_blank_state_spec.js @@ -1,4 +1,3 @@ -/* global BoardService */ import Vue from 'vue'; import '~/boards/stores/boards_store'; import BoardBlankState from '~/boards/components/board_blank_state.vue'; diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js index 13d607a06d2..9b4db774b63 100644 --- a/spec/javascripts/boards/board_card_spec.js +++ b/spec/javascripts/boards/board_card_spec.js @@ -1,7 +1,6 @@ /* global List */ /* global ListAssignee */ /* global ListLabel */ -/* global BoardService */ import Vue from 'vue'; import MockAdapter from 'axios-mock-adapter'; diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js index c06b2f60813..a49b190d36a 100644 --- a/spec/javascripts/boards/board_list_spec.js +++ b/spec/javascripts/boards/board_list_spec.js @@ -1,4 +1,3 @@ -/* global BoardService */ /* global List */ /* global ListIssue */ import Vue from 'vue'; diff --git a/spec/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js index d5fbfdeaa91..ee37821ad08 100644 --- a/spec/javascripts/boards/board_new_issue_spec.js +++ b/spec/javascripts/boards/board_new_issue_spec.js @@ -1,4 +1,3 @@ -/* global BoardService */ /* global List */ import Vue from 'vue'; diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js index 0cf9e4c9ba1..46fa10e1789 100644 --- a/spec/javascripts/boards/boards_store_spec.js +++ b/spec/javascripts/boards/boards_store_spec.js @@ -1,5 +1,4 @@ /* eslint-disable comma-dangle, one-var, no-unused-vars */ -/* global BoardService */ /* global ListIssue */ import Vue from 'vue'; diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js index 4a11131b55c..d90f9a41231 100644 --- a/spec/javascripts/boards/issue_spec.js +++ b/spec/javascripts/boards/issue_spec.js @@ -1,5 +1,4 @@ /* eslint-disable comma-dangle */ -/* global BoardService */ /* global ListIssue */ import Vue from 'vue'; diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js index d9a1d692949..d5d1139de15 100644 --- a/spec/javascripts/boards/list_spec.js +++ b/spec/javascripts/boards/list_spec.js @@ -1,5 +1,4 @@ /* eslint-disable comma-dangle */ -/* global BoardService */ /* global List */ /* global ListIssue */ diff --git a/spec/javascripts/commit/commit_pipeline_status_component_spec.js b/spec/javascripts/commit/commit_pipeline_status_component_spec.js index 421fe62a1e7..d3776d0c3cf 100644 --- a/spec/javascripts/commit/commit_pipeline_status_component_spec.js +++ b/spec/javascripts/commit/commit_pipeline_status_component_spec.js @@ -75,10 +75,7 @@ describe('Commit pipeline status component', () => { describe('When polling data was not succesful', () => { beforeEach(() => { mock = new MockAdapter(axios); - mock.onGet('/dummy/endpoint').reply(() => { - const res = Promise.reject([502, { }]); - return res; - }); + mock.onGet('/dummy/endpoint').reply(502, {}); vm = new Component({ props: { endpoint: '/dummy/endpoint', diff --git a/spec/javascripts/helpers/vue_mount_component_helper.js b/spec/javascripts/helpers/vue_mount_component_helper.js index a34a1add4e0..5ba17ecf5b5 100644 --- a/spec/javascripts/helpers/vue_mount_component_helper.js +++ b/spec/javascripts/helpers/vue_mount_component_helper.js @@ -1,30 +1,18 @@ -import Vue from 'vue'; - -const mountComponent = (Component, props = {}, el = null) => new Component({ - propsData: props, -}).$mount(el); - -export const createComponentWithStore = (Component, store, propsData = {}) => new Component({ - store, - propsData, -}); +const mountComponent = (Component, props = {}, el = null) => + new Component({ + propsData: props, + }).$mount(el); -export const createComponentWithMixin = (mixins = [], state = {}, props = {}, template = '<div></div>') => { - const Component = Vue.extend({ - template, - mixins, - data() { - return props; - }, +export const createComponentWithStore = (Component, store, propsData = {}) => + new Component({ + store, + propsData, }); - return mountComponent(Component, props); -}; - export const mountComponentWithStore = (Component, { el, props, store }) => new Component({ store, - propsData: props || { }, + propsData: props || {}, }).$mount(el); export default mountComponent; diff --git a/spec/javascripts/ide/components/external_link_spec.js b/spec/javascripts/ide/components/external_link_spec.js new file mode 100644 index 00000000000..b3d94c041fa --- /dev/null +++ b/spec/javascripts/ide/components/external_link_spec.js @@ -0,0 +1,35 @@ +import Vue from 'vue'; +import externalLink from '~/ide/components/external_link.vue'; +import createVueComponent from '../../helpers/vue_mount_component_helper'; +import { file } from '../helpers'; + +describe('ExternalLink', () => { + const activeFile = file(); + let vm; + + function createComponent() { + const ExternalLink = Vue.extend(externalLink); + + activeFile.permalink = 'test'; + + return createVueComponent(ExternalLink, { + file: activeFile, + }); + } + + afterEach(() => { + vm.$destroy(); + }); + + it('renders the external link with the correct href', done => { + activeFile.binary = true; + vm = createComponent(); + + vm.$nextTick(() => { + const openLink = vm.$el.querySelector('a'); + + expect(openLink.href).toMatch(`/${activeFile.permalink}`); + done(); + }); + }); +}); diff --git a/spec/javascripts/ide/components/ide_file_buttons_spec.js b/spec/javascripts/ide/components/ide_file_buttons_spec.js deleted file mode 100644 index 8ac8d1b2acf..00000000000 --- a/spec/javascripts/ide/components/ide_file_buttons_spec.js +++ /dev/null @@ -1,61 +0,0 @@ -import Vue from 'vue'; -import repoFileButtons from '~/ide/components/ide_file_buttons.vue'; -import createVueComponent from '../../helpers/vue_mount_component_helper'; -import { file } from '../helpers'; - -describe('RepoFileButtons', () => { - const activeFile = file(); - let vm; - - function createComponent() { - const RepoFileButtons = Vue.extend(repoFileButtons); - - activeFile.rawPath = 'test'; - activeFile.blamePath = 'test'; - activeFile.commitsPath = 'test'; - - return createVueComponent(RepoFileButtons, { - file: activeFile, - }); - } - - afterEach(() => { - vm.$destroy(); - }); - - it('renders Raw, Blame, History and Permalink', done => { - vm = createComponent(); - - vm.$nextTick(() => { - const raw = vm.$el.querySelector('.raw'); - const blame = vm.$el.querySelector('.blame'); - const history = vm.$el.querySelector('.history'); - - expect(raw.href).toMatch(`/${activeFile.rawPath}`); - expect(raw.getAttribute('data-original-title')).toEqual('Raw'); - expect(blame.href).toMatch(`/${activeFile.blamePath}`); - expect(blame.getAttribute('data-original-title')).toEqual('Blame'); - expect(history.href).toMatch(`/${activeFile.commitsPath}`); - expect(history.getAttribute('data-original-title')).toEqual('History'); - expect(vm.$el.querySelector('.permalink').getAttribute('data-original-title')).toEqual( - 'Permalink', - ); - - done(); - }); - }); - - it('renders Download', done => { - activeFile.binary = true; - vm = createComponent(); - - vm.$nextTick(() => { - const raw = vm.$el.querySelector('.raw'); - - expect(raw.href).toMatch(`/${activeFile.rawPath}`); - expect(raw.getAttribute('data-original-title')).toEqual('Download'); - - done(); - }); - }); -}); diff --git a/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js b/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js index 0c173062835..6110d5d89ac 100644 --- a/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js +++ b/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js @@ -8,10 +8,7 @@ describe('Confidential Issue Sidebar Block', () => { beforeEach(() => { const Component = Vue.extend(confidentialIssueSidebar); const service = { - update: () => new Promise((resolve, reject) => { - resolve(true); - reject('failed!'); - }), + update: () => Promise.resolve(true), }; vm1 = new Component({ diff --git a/spec/javascripts/u2f/mock_u2f_device.js b/spec/javascripts/u2f/mock_u2f_device.js index 5a1ace2b4d6..8fec6ae3fa4 100644 --- a/spec/javascripts/u2f/mock_u2f_device.js +++ b/spec/javascripts/u2f/mock_u2f_device.js @@ -1,5 +1,5 @@ /* eslint-disable prefer-rest-params, wrap-iife, -no-unused-expressions, no-return-assign, no-param-reassign*/ +no-unused-expressions, no-return-assign, no-param-reassign */ export default class MockU2FDevice { constructor() { diff --git a/spec/lib/gitlab/auth/request_authenticator_spec.rb b/spec/lib/gitlab/auth/request_authenticator_spec.rb index ffcd90b9fcb..242ab4a91dd 100644 --- a/spec/lib/gitlab/auth/request_authenticator_spec.rb +++ b/spec/lib/gitlab/auth/request_authenticator_spec.rb @@ -39,19 +39,19 @@ describe Gitlab::Auth::RequestAuthenticator do describe '#find_sessionless_user' do let!(:access_token_user) { build(:user) } - let!(:rss_token_user) { build(:user) } + let!(:feed_token_user) { build(:user) } it 'returns access_token user first' do allow_any_instance_of(described_class).to receive(:find_user_from_access_token).and_return(access_token_user) - allow_any_instance_of(described_class).to receive(:find_user_from_rss_token).and_return(rss_token_user) + allow_any_instance_of(described_class).to receive(:find_user_from_feed_token).and_return(feed_token_user) expect(subject.find_sessionless_user).to eq access_token_user end - it 'returns rss_token user if no access_token user found' do - allow_any_instance_of(described_class).to receive(:find_user_from_rss_token).and_return(rss_token_user) + it 'returns feed_token user if no access_token user found' do + allow_any_instance_of(described_class).to receive(:find_user_from_feed_token).and_return(feed_token_user) - expect(subject.find_sessionless_user).to eq rss_token_user + expect(subject.find_sessionless_user).to eq feed_token_user end it 'returns nil if no user found' do diff --git a/spec/lib/gitlab/auth/user_auth_finders_spec.rb b/spec/lib/gitlab/auth/user_auth_finders_spec.rb index 2733eef6611..136646bd4ee 100644 --- a/spec/lib/gitlab/auth/user_auth_finders_spec.rb +++ b/spec/lib/gitlab/auth/user_auth_finders_spec.rb @@ -46,34 +46,54 @@ describe Gitlab::Auth::UserAuthFinders do end end - describe '#find_user_from_rss_token' do + describe '#find_user_from_feed_token' do context 'when the request format is atom' do before do env['HTTP_ACCEPT'] = 'application/atom+xml' end - it 'returns user if valid rss_token' do - set_param(:rss_token, user.rss_token) + context 'when feed_token param is provided' do + it 'returns user if valid feed_token' do + set_param(:feed_token, user.feed_token) - expect(find_user_from_rss_token).to eq user - end + expect(find_user_from_feed_token).to eq user + end + + it 'returns nil if feed_token is blank' do + expect(find_user_from_feed_token).to be_nil + end + + it 'returns exception if invalid feed_token' do + set_param(:feed_token, 'invalid_token') - it 'returns nil if rss_token is blank' do - expect(find_user_from_rss_token).to be_nil + expect { find_user_from_feed_token }.to raise_error(Gitlab::Auth::UnauthorizedError) + end end - it 'returns exception if invalid rss_token' do - set_param(:rss_token, 'invalid_token') + context 'when rss_token param is provided' do + it 'returns user if valid rssd_token' do + set_param(:rss_token, user.feed_token) - expect { find_user_from_rss_token }.to raise_error(Gitlab::Auth::UnauthorizedError) + expect(find_user_from_feed_token).to eq user + end + + it 'returns nil if rss_token is blank' do + expect(find_user_from_feed_token).to be_nil + end + + it 'returns exception if invalid rss_token' do + set_param(:rss_token, 'invalid_token') + + expect { find_user_from_feed_token }.to raise_error(Gitlab::Auth::UnauthorizedError) + end end end context 'when the request format is not atom' do it 'returns nil' do - set_param(:rss_token, user.rss_token) + set_param(:feed_token, user.feed_token) - expect(find_user_from_rss_token).to be_nil + expect(find_user_from_feed_token).to be_nil end end @@ -81,7 +101,7 @@ describe Gitlab::Auth::UserAuthFinders do it 'the method call does not modify the original value' do env['action_dispatch.request.formats'] = nil - find_user_from_rss_token + find_user_from_feed_token expect(env['action_dispatch.request.formats']).to be_nil end diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb index c63120b0b29..05c232d22cf 100644 --- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb @@ -19,6 +19,18 @@ describe Gitlab::BitbucketImport::Importer do ] end + let(:reporters) do + [ + nil, + { "username" => "reporter1" }, + nil, + { "username" => "reporter2" }, + { "username" => "reporter1" }, + nil, + { "username" => "reporter3" } + ] + end + let(:sample_issues_statuses) do issues = [] @@ -36,6 +48,10 @@ describe Gitlab::BitbucketImport::Importer do } end + reporters.map.with_index do |reporter, index| + issues[index]['reporter'] = reporter + end + issues end @@ -147,5 +163,19 @@ describe Gitlab::BitbucketImport::Importer do expect(importer.errors).to be_empty end end + + describe 'issue import' do + it 'maps reporters to anonymous if bitbucket reporter is nil' do + allow(importer).to receive(:import_wiki) + importer.execute + + expect(project.issues.size).to eq(7) + expect(project.issues.where("description LIKE ?", '%Anonymous%').size).to eq(3) + expect(project.issues.where("description LIKE ?", '%reporter1%').size).to eq(2) + expect(project.issues.where("description LIKE ?", '%reporter2%').size).to eq(1) + expect(project.issues.where("description LIKE ?", '%reporter3%').size).to eq(1) + expect(importer.errors).to be_empty + end + end end end diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index e2547ed0311..94eaf86ef80 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -22,6 +22,12 @@ describe Gitlab::Git::Blob, seed_helper: true do it { expect(blob).to eq(nil) } end + context 'utf-8 branch' do + let(:blob) { Gitlab::Git::Blob.find(repository, 'Ääh-test-utf-8', "files/ruby/popen.rb")} + + it { expect(blob.id).to eq(SeedRepo::RubyBlob::ID) } + end + context 'blank path' do let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, '') } diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 08c6d1e55e9..89be8a1b7f2 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -603,8 +603,8 @@ describe Gitlab::Git::Commit, seed_helper: true do let(:commit) { described_class.find(repository, 'master') } subject { commit.ref_names(repository) } - it 'has 1 element' do - expect(subject.size).to eq(1) + it 'has 2 element' do + expect(subject.size).to eq(2) end it { is_expected.to include("master") } it { is_expected.not_to include("feature") } diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index af6a486ab20..dd5c498706d 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1374,7 +1374,7 @@ describe Gitlab::Git::Repository, seed_helper: true do describe '#branch_count' do it 'returns the number of branches' do - expect(repository.branch_count).to eq(10) + expect(repository.branch_count).to eq(11) end context 'with local and remote branches' do @@ -2248,7 +2248,7 @@ describe Gitlab::Git::Repository, seed_helper: true do describe '#checksum' do it 'calculates the checksum for non-empty repo' do - expect(repository.checksum).to eq '54f21be4c32c02f6788d72207fa03ad3bce725e4' + expect(repository.checksum).to eq '4be7d24ce7e8d845502d599b72d567d23e6a40c0' end it 'returns 0000000000000000000000000000000000000000 for an empty repo' do diff --git a/spec/lib/gitlab/gitlab_import/importer_spec.rb b/spec/lib/gitlab/gitlab_import/importer_spec.rb index e1d935602b5..200edceca8c 100644 --- a/spec/lib/gitlab/gitlab_import/importer_spec.rb +++ b/spec/lib/gitlab/gitlab_import/importer_spec.rb @@ -20,7 +20,7 @@ describe Gitlab::GitlabImport::Importer do } } ]) - stub_request('issues/2579857/notes', []) + stub_request('issues/3/notes', []) end it 'persists issues' do @@ -43,7 +43,7 @@ describe Gitlab::GitlabImport::Importer do end def stub_request(path, body) - url = "https://gitlab.com/api/v3/projects/asd%2Fvim/#{path}?page=1&per_page=100" + url = "https://gitlab.com/api/v4/projects/asd%2Fvim/#{path}?page=1&per_page=100" WebMock.stub_request(:get, url) .to_return( diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index fb5fd300dbb..a129855dbd8 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -310,3 +310,8 @@ lfs_file_locks: - user project_badges: - project +metrics: +- merge_request +- latest_closed_by +- merged_by +- pipeline diff --git a/spec/lib/gitlab/import_export/relation_factory_spec.rb b/spec/lib/gitlab/import_export/relation_factory_spec.rb index 5f0dfd64b15..cf9e0f71910 100644 --- a/spec/lib/gitlab/import_export/relation_factory_spec.rb +++ b/spec/lib/gitlab/import_export/relation_factory_spec.rb @@ -119,6 +119,25 @@ describe Gitlab::ImportExport::RelationFactory do end end + context 'overrided model with pluralized name' do + let(:relation_sym) { :metrics } + + let(:relation_hash) do + { + 'id' => 99, + 'merge_request_id' => 99, + 'merged_at' => Time.now, + 'merged_by_id' => 99, + 'latest_closed_at' => nil, + 'latest_closed_by_id' => nil + } + end + + it 'does not raise errors' do + expect { created_object }.not_to raise_error + end + end + context 'Project references' do let(:relation_sym) { :project_foo_model } let(:relation_hash) do diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 2619c6a10d8..2aebdc57f7c 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -165,6 +165,7 @@ MergeRequest: - approvals_before_merge - rebase_commit_sha - time_estimate +- squash - last_edited_at - last_edited_by_id - head_pipeline_id @@ -204,6 +205,19 @@ MergeRequestDiffFile: - b_mode - too_large - binary +MergeRequest::Metrics: +- id +- created_at +- updated_at +- merge_request_id +- pipeline_id +- latest_closed_by_id +- latest_closed_at +- merged_by_id +- merged_at +- latest_build_started_at +- latest_build_finished_at +- first_deployed_to_production_at Ci::Pipeline: - id - project_id diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb index 8ba15ae0f38..7c194749dfb 100644 --- a/spec/lib/mattermost/command_spec.rb +++ b/spec/lib/mattermost/command_spec.rb @@ -21,13 +21,13 @@ describe Mattermost::Command do context 'for valid trigger word' do before do - stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create') + stub_request(:post, 'http://mattermost.example.com/api/v4/commands') .with(body: { team_id: 'abc', trigger: 'gitlab' }.to_json) .to_return( - status: 200, + status: 201, headers: { 'Content-Type' => 'application/json' }, body: { token: 'token' }.to_json ) @@ -40,16 +40,16 @@ describe Mattermost::Command do context 'for error message' do before do - stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create') + stub_request(:post, 'http://mattermost.example.com/api/v4/commands') .to_return( - status: 500, + status: 400, headers: { 'Content-Type' => 'application/json' }, body: { id: 'api.command.duplicate_trigger.app_error', message: 'This trigger word is already in use. Please choose another word.', detailed_error: '', request_id: 'obc374man7bx5r3dbc1q5qhf3r', - status_code: 500 + status_code: 400 }.to_json ) end diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb index c855643c4d8..5410bfbeb31 100644 --- a/spec/lib/mattermost/session_spec.rb +++ b/spec/lib/mattermost/session_spec.rb @@ -22,8 +22,8 @@ describe Mattermost::Session, type: :request do let(:location) { 'http://location.tld' } let(:cookie_header) {'MMOAUTH=taskik8az7rq8k6rkpuas7htia; Path=/;'} let!(:stub) do - WebMock.stub_request(:get, "#{mattermost_url}/api/v3/oauth/gitlab/login") - .to_return(headers: { 'location' => location, 'Set-Cookie' => cookie_header }, status: 307) + WebMock.stub_request(:get, "#{mattermost_url}/oauth/gitlab/login") + .to_return(headers: { 'location' => location, 'Set-Cookie' => cookie_header }, status: 302) end context 'without oauth uri' do @@ -76,7 +76,7 @@ describe Mattermost::Session, type: :request do end end - WebMock.stub_request(:post, "#{mattermost_url}/api/v3/users/logout") + WebMock.stub_request(:post, "#{mattermost_url}/api/v4/users/logout") .to_return(headers: { Authorization: 'token thisworksnow' }, status: 200) end diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb index 2cfa6802612..030aa5d06a8 100644 --- a/spec/lib/mattermost/team_spec.rb +++ b/spec/lib/mattermost/team_spec.rb @@ -12,26 +12,28 @@ describe Mattermost::Team do describe '#all' do subject { described_class.new(nil).all } + let(:test_team) do + { + "id" => "xiyro8huptfhdndadpz8r3wnbo", + "create_at" => 1482174222155, + "update_at" => 1482174222155, + "delete_at" => 0, + "display_name" => "chatops", + "name" => "chatops", + "email" => "admin@example.com", + "type" => "O", + "company_name" => "", + "allowed_domains" => "", + "invite_id" => "o4utakb9jtb7imctdfzbf9r5ro", + "allow_open_invite" => false + } + end + context 'for valid request' do - let(:response) do - { "xiyro8huptfhdndadpz8r3wnbo" => { - "id" => "xiyro8huptfhdndadpz8r3wnbo", - "create_at" => 1482174222155, - "update_at" => 1482174222155, - "delete_at" => 0, - "display_name" => "chatops", - "name" => "chatops", - "email" => "admin@example.com", - "type" => "O", - "company_name" => "", - "allowed_domains" => "", - "invite_id" => "o4utakb9jtb7imctdfzbf9r5ro", - "allow_open_invite" => false - } } - end + let(:response) { [test_team] } before do - stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all') + stub_request(:get, 'http://mattermost.example.com/api/v4/users/me/teams') .to_return( status: 200, headers: { 'Content-Type' => 'application/json' }, @@ -39,14 +41,14 @@ describe Mattermost::Team do ) end - it 'returns a token' do - is_expected.to eq(response.values) + it 'returns teams' do + is_expected.to eq(response) end end context 'for error message' do before do - stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all') + stub_request(:get, 'http://mattermost.example.com/api/v4/users/me/teams') .to_return( status: 500, headers: { 'Content-Type' => 'application/json' }, @@ -89,9 +91,9 @@ describe Mattermost::Team do end before do - stub_request(:post, "http://mattermost.example.com/api/v3/teams/create") + stub_request(:post, "http://mattermost.example.com/api/v4/teams") .to_return( - status: 200, + status: 201, body: response.to_json, headers: { 'Content-Type' => 'application/json' } ) @@ -104,7 +106,7 @@ describe Mattermost::Team do context 'for existing team' do before do - stub_request(:post, 'http://mattermost.example.com/api/v3/teams/create') + stub_request(:post, 'http://mattermost.example.com/api/v4/teams') .to_return( status: 400, headers: { 'Content-Type' => 'application/json' }, diff --git a/spec/migrations/migrate_remaining_mr_metrics_populating_background_migration_spec.rb b/spec/migrations/migrate_remaining_mr_metrics_populating_background_migration_spec.rb new file mode 100644 index 00000000000..47dab18183c --- /dev/null +++ b/spec/migrations/migrate_remaining_mr_metrics_populating_background_migration_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20180521162137_migrate_remaining_mr_metrics_populating_background_migration.rb') + +describe MigrateRemainingMrMetricsPopulatingBackgroundMigration, :migration, :sidekiq do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:mrs) { table(:merge_requests) } + + before do + namespaces.create!(id: 1, name: 'foo', path: 'foo') + projects.create!(id: 123, name: 'gitlab1', path: 'gitlab1', namespace_id: 1) + projects.create!(id: 456, name: 'gitlab2', path: 'gitlab2', namespace_id: 1) + projects.create!(id: 789, name: 'gitlab3', path: 'gitlab3', namespace_id: 1) + mrs.create!(title: 'foo', target_branch: 'target', source_branch: 'source', target_project_id: 123) + mrs.create!(title: 'bar', target_branch: 'target', source_branch: 'source', target_project_id: 456) + mrs.create!(title: 'kux', target_branch: 'target', source_branch: 'source', target_project_id: 789) + end + + it 'correctly schedules background migrations' do + stub_const("#{described_class.name}::BATCH_SIZE", 2) + + Sidekiq::Testing.fake! do + Timecop.freeze do + migrate! + + expect(described_class::MIGRATION) + .to be_scheduled_delayed_migration(10.minutes, mrs.first.id, mrs.second.id) + + expect(described_class::MIGRATION) + .to be_scheduled_delayed_migration(20.minutes, mrs.third.id, mrs.third.id) + + expect(BackgroundMigrationWorker.jobs.size).to eq(2) + end + end + end +end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index f8f07205623..968267a6d24 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -195,22 +195,6 @@ describe ApplicationSetting do expect(setting.pick_repository_storage).to eq('random') end - - describe '#repository_storage' do - it 'returns the first storage' do - setting.repository_storages = %w(good bad) - - expect(setting.repository_storage).to eq('good') - end - end - - describe '#repository_storage=' do - it 'overwrites repository_storages' do - setting.repository_storage = 'overwritten' - - expect(setting.repository_storages).to eq(['overwritten']) - end - end end end diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb index dfb83578fce..9b804429138 100644 --- a/spec/models/concerns/token_authenticatable_spec.rb +++ b/spec/models/concerns/token_authenticatable_spec.rb @@ -12,7 +12,7 @@ shared_examples 'TokenAuthenticatable' do end describe User, 'TokenAuthenticatable' do - let(:token_field) { :rss_token } + let(:token_field) { :feed_token } it_behaves_like 'TokenAuthenticatable' describe 'ensures authentication token' do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index aa968dda41f..65cc9372cbe 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -14,6 +14,65 @@ describe MergeRequest do it { is_expected.to have_many(:merge_request_diffs) } end + describe '#squash_in_progress?' do + shared_examples 'checking whether a squash is in progress' do + let(:repo_path) { subject.source_project.repository.path } + let(:squash_path) { File.join(repo_path, "gitlab-worktree", "squash-#{subject.id}") } + + before do + system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} worktree add --detach #{squash_path} master)) + end + + it 'returns true when there is a current squash directory' do + expect(subject.squash_in_progress?).to be_truthy + end + + it 'returns false when there is no squash directory' do + FileUtils.rm_rf(squash_path) + + expect(subject.squash_in_progress?).to be_falsey + end + + it 'returns false when the squash directory has expired' do + time = 20.minutes.ago.to_time + File.utime(time, time, squash_path) + + expect(subject.squash_in_progress?).to be_falsey + end + + it 'returns false when the source project has been removed' do + allow(subject).to receive(:source_project).and_return(nil) + + expect(subject.squash_in_progress?).to be_falsey + end + end + + context 'when Gitaly squash_in_progress is enabled' do + it_behaves_like 'checking whether a squash is in progress' + end + + context 'when Gitaly squash_in_progress is disabled', :disable_gitaly do + it_behaves_like 'checking whether a squash is in progress' + end + end + + describe '#squash?' do + let(:merge_request) { build(:merge_request, squash: squash) } + subject { merge_request.squash? } + + context 'disabled in database' do + let(:squash) { false } + + it { is_expected.to be_falsy } + end + + context 'enabled in database' do + let(:squash) { true } + + it { is_expected.to be_truthy } + end + end + describe 'modules' do subject { described_class } diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 3be023a48c1..68ab9fd08ec 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -65,7 +65,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do before do kubernetes_service.update_attribute(:active, false) - kubernetes_service.properties[:namespace] = "foo" + kubernetes_service.properties['namespace'] = "foo" end it 'should not update attributes' do @@ -82,7 +82,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do let(:kubernetes_service) { create(:kubernetes_service) } it 'should update attributes' do - kubernetes_service.properties[:namespace] = 'foo' + kubernetes_service.properties['namespace'] = 'foo' expect(kubernetes_service.save).to be_truthy end end @@ -92,7 +92,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do before do kubernetes_service.active = false - kubernetes_service.properties[:namespace] = 'foo' + kubernetes_service.properties['namespace'] = 'foo' kubernetes_service.save end @@ -105,7 +105,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do end it 'should update attributes' do - expect(kubernetes_service.properties[:namespace]).to eq("foo") + expect(kubernetes_service.properties['namespace']).to eq("foo") end end @@ -113,12 +113,12 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do let(:kubernetes_service) { create(:kubernetes_service, template: true, active: false) } before do - kubernetes_service.properties[:namespace] = 'foo' + kubernetes_service.properties['namespace'] = 'foo' end it 'should update attributes' do expect(kubernetes_service.save).to be_truthy - expect(kubernetes_service.properties[:namespace]).to eq('foo') + expect(kubernetes_service.properties['namespace']).to eq('foo') end end end diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb index 05d33cd3874..1983e0cc967 100644 --- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb +++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb @@ -25,7 +25,7 @@ describe MattermostSlashCommandsService do context 'the requests succeeds' do before do - stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create') + stub_request(:post, 'http://mattermost.example.com/api/v4/commands') .with(body: { team_id: 'abc', trigger: 'gitlab', @@ -59,7 +59,7 @@ describe MattermostSlashCommandsService do context 'an error is received' do before do - stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create') + stub_request(:post, 'http://mattermost.example.com/api/v4/commands') .to_return( status: 500, headers: { 'Content-Type' => 'application/json' }, @@ -89,11 +89,11 @@ describe MattermostSlashCommandsService do context 'the requests succeeds' do before do - stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all') + stub_request(:get, 'http://mattermost.example.com/api/v4/users/me/teams') .to_return( status: 200, headers: { 'Content-Type' => 'application/json' }, - body: { 'list' => true }.to_json + body: [{ id: 'test_team_id' }].to_json ) end @@ -104,7 +104,7 @@ describe MattermostSlashCommandsService do context 'an error is received' do before do - stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all') + stub_request(:get, 'http://mattermost.example.com/api/v4/users/me/teams') .to_return( status: 500, headers: { 'Content-Type' => 'application/json' }, diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 6a2f4a39f09..16b409844fa 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -644,13 +644,13 @@ describe User do end end - describe 'rss token' do - it 'ensures an rss token on read' do - user = create(:user, rss_token: nil) - rss_token = user.rss_token + describe 'feed token' do + it 'ensures a feed token on read' do + user = create(:user, feed_token: nil) + feed_token = user.feed_token - expect(rss_token).not_to be_blank - expect(user.reload.rss_token).to eq rss_token + expect(feed_token).not_to be_blank + expect(user.reload.feed_token).to eq feed_token end end diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb index efd175247b5..6dfaa3b72f7 100644 --- a/spec/presenters/ci/build_presenter_spec.rb +++ b/spec/presenters/ci/build_presenter_spec.rb @@ -219,11 +219,11 @@ describe Ci::BuildPresenter do end describe '#callout_failure_message' do - let(:build) { create(:ci_build, :failed, :script_failure) } + let(:build) { create(:ci_build, :failed, :api_failure) } it 'returns a verbose failure reason' do description = subject.callout_failure_message - expect(description).to eq('There has been a script failure. Check the job log for more information') + expect(description).to eq('There has been an API failure, please try again') end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 1eeeb4f1045..605761867bf 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -29,6 +29,18 @@ describe API::MergeRequests do project.add_reporter(user) end + describe 'route shadowing' do + include GrapePathHelpers::NamedRouteMatcher + + it 'does not occur' do + path = api_v4_projects_merge_requests_path(id: 1) + expect(path).to eq('/api/v4/projects/1/merge_requests') + + path = api_v4_projects_merge_requests_path(id: 1, merge_request_iid: 3) + expect(path).to eq('/api/v4/projects/1/merge_requests/3') + end + end + describe 'GET /merge_requests' do context 'when unauthenticated' do it 'returns an array of all merge requests' do @@ -60,12 +72,6 @@ describe API::MergeRequests do expect(response).to have_gitlab_http_status(401) end - - it "returns authentication error when scope is created_by_me" do - get api("/merge_requests"), scope: 'created_by_me' - - expect(response).to have_gitlab_http_status(401) - end end context 'when authenticated' do @@ -217,223 +223,46 @@ describe API::MergeRequests do end describe "GET /projects/:id/merge_requests" do - context "when unauthenticated" do - it 'returns merge requests for public projects' do - get api("/projects/#{project.id}/merge_requests") - - expect_paginated_array_response - end - - it "returns 404 for non public projects" do - project = create(:project, :private) - get api("/projects/#{project.id}/merge_requests") - - expect(response).to have_gitlab_http_status(404) - end - end - - context "when authenticated" do - it 'avoids N+1 queries' do - control = ActiveRecord::QueryRecorder.new do - get api("/projects/#{project.id}/merge_requests", user) - end - - create(:merge_request, state: 'closed', milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) - - create(:merge_request, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) - - expect do - get api("/projects/#{project.id}/merge_requests", user) - end.not_to exceed_query_limit(control) - end - - it "returns an array of all merge_requests" do - get api("/projects/#{project.id}/merge_requests", user) - - expect_response_ordered_exactly(merge_request_merged, merge_request_closed, merge_request) - expect(json_response.last['title']).to eq(merge_request.title) - expect(json_response.last).to have_key('web_url') - expect(json_response.last['sha']).to eq(merge_request.diff_head_sha) - expect(json_response.last['merge_commit_sha']).to be_nil - expect(json_response.last['merge_commit_sha']).to eq(merge_request.merge_commit_sha) - expect(json_response.last['downvotes']).to eq(1) - expect(json_response.last['upvotes']).to eq(1) - expect(json_response.last['labels']).to eq([label2.title, label.title]) - expect(json_response.first['title']).to eq(merge_request_merged.title) - expect(json_response.first['sha']).to eq(merge_request_merged.diff_head_sha) - expect(json_response.first['merge_commit_sha']).not_to be_nil - expect(json_response.first['merge_commit_sha']).to eq(merge_request_merged.merge_commit_sha) - end - - it "returns an array of all merge_requests using simple mode" do - get api("/projects/#{project.id}/merge_requests?view=simple", user) - - expect_response_ordered_exactly(merge_request_merged, merge_request_closed, merge_request) - expect(json_response.last.keys).to match_array(%w(id iid title web_url created_at description project_id state updated_at)) - expect(json_response.last['iid']).to eq(merge_request.iid) - expect(json_response.last['title']).to eq(merge_request.title) - expect(json_response.last).to have_key('web_url') - expect(json_response.first['iid']).to eq(merge_request_merged.iid) - expect(json_response.first['title']).to eq(merge_request_merged.title) - expect(json_response.first).to have_key('web_url') - end - - it "returns an array of all merge_requests" do - get api("/projects/#{project.id}/merge_requests?state", user) - - expect_response_ordered_exactly(merge_request_merged, merge_request_closed, merge_request) - expect(json_response.last['title']).to eq(merge_request.title) - end - - it "returns an array of open merge_requests" do - get api("/projects/#{project.id}/merge_requests?state=opened", user) - - expect_response_ordered_exactly(merge_request) - expect(json_response.last['title']).to eq(merge_request.title) - end - - it "returns an array of closed merge_requests" do - get api("/projects/#{project.id}/merge_requests?state=closed", user) - - expect_response_ordered_exactly(merge_request_closed) - expect(json_response.first['title']).to eq(merge_request_closed.title) - end - - it "returns an array of merged merge_requests" do - get api("/projects/#{project.id}/merge_requests?state=merged", user) - - expect_response_ordered_exactly(merge_request_merged) - expect(json_response.first['title']).to eq(merge_request_merged.title) - end - - it 'returns merge_request by "iids" array' do - get api("/projects/#{project.id}/merge_requests", user), iids: [merge_request.iid, merge_request_closed.iid] - - expect_response_ordered_exactly(merge_request_closed, merge_request) - expect(json_response.first['title']).to eq merge_request_closed.title - end - - it 'matches V4 response schema' do - get api("/projects/#{project.id}/merge_requests", user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to match_response_schema('public_api/v4/merge_requests') - end - - it 'returns an empty array if no issue matches milestone' do - get api("/projects/#{project.id}/merge_requests", user), milestone: '1.0.0' - - expect_paginated_array_response - expect(json_response.length).to eq(0) - end - - it 'returns an empty array if milestone does not exist' do - get api("/projects/#{project.id}/merge_requests", user), milestone: 'foo' - - expect_paginated_array_response - expect(json_response.length).to eq(0) - end - - it 'returns an array of merge requests in given milestone' do - get api("/projects/#{project.id}/merge_requests", user), milestone: '0.9' - - expect(json_response.first['title']).to eq merge_request_closed.title - expect(json_response.first['id']).to eq merge_request_closed.id - end - - it 'returns an array of merge requests matching state in milestone' do - get api("/projects/#{project.id}/merge_requests", user), milestone: '0.9', state: 'closed' - - expect_response_ordered_exactly(merge_request_closed) - end - - it 'returns an array of labeled merge requests' do - get api("/projects/#{project.id}/merge_requests?labels=#{label.title}", user) - - expect_paginated_array_response - expect(json_response.length).to eq(1) - expect(json_response.first['labels']).to eq([label2.title, label.title]) - end - - it 'returns an array of labeled merge requests where all labels match' do - get api("/projects/#{project.id}/merge_requests?labels=#{label.title},foo,bar", user) - - expect_paginated_array_response - expect(json_response.length).to eq(0) - end - - it 'returns an empty array if no merge request matches labels' do - get api("/projects/#{project.id}/merge_requests?labels=foo,bar", user) - - expect_paginated_array_response - expect(json_response.length).to eq(0) - end - - it 'returns an array of labeled merge requests that are merged for a milestone' do - bug_label = create(:label, title: 'bug', color: '#FFAABB', project: project) - - mr1 = create(:merge_request, state: "merged", source_project: project, target_project: project, milestone: milestone) - mr2 = create(:merge_request, state: "merged", source_project: project, target_project: project, milestone: milestone1) - mr3 = create(:merge_request, state: "closed", source_project: project, target_project: project, milestone: milestone1) - _mr = create(:merge_request, state: "merged", source_project: project, target_project: project, milestone: milestone1) + let(:endpoint_path) { "/projects/#{project.id}/merge_requests" } - create(:label_link, label: bug_label, target: mr1) - create(:label_link, label: bug_label, target: mr2) - create(:label_link, label: bug_label, target: mr3) + it_behaves_like 'merge requests list' - get api("/projects/#{project.id}/merge_requests?labels=#{bug_label.title}&milestone=#{milestone1.title}&state=merged", user) - - expect_response_ordered_exactly(mr2) - end - - context "with ordering" do - let(:merge_requests) { [merge_request_merged, merge_request_closed, merge_request] } - - before do - @mr_later = mr_with_later_created_and_updated_at_time - @mr_earlier = mr_with_earlier_created_and_updated_at_time - end - - it "returns an array of merge_requests in ascending order" do - get api("/projects/#{project.id}/merge_requests?sort=asc", user) - - expect_response_ordered_exactly(*merge_requests.sort_by { |mr| mr['created_at'] }) - end - - it "returns an array of merge_requests in descending order" do - get api("/projects/#{project.id}/merge_requests?sort=desc", user) + it "returns 404 for non public projects" do + project = create(:project, :private) - expect_response_ordered_exactly(*merge_requests.sort_by { |mr| mr['created_at'] }.reverse) - end + get api("/projects/#{project.id}/merge_requests") - it "returns an array of merge_requests ordered by updated_at" do - get api("/projects/#{project.id}/merge_requests?order_by=updated_at", user) + expect(response).to have_gitlab_http_status(404) + end - expect_response_ordered_exactly(*merge_requests.sort_by { |mr| mr['updated_at'] }.reverse) - end + it 'returns merge_request by "iids" array' do + get api(endpoint_path, user), iids: [merge_request.iid, merge_request_closed.iid] - it "returns an array of merge_requests ordered by created_at" do - get api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user) + expect(response).to have_gitlab_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['title']).to eq merge_request_closed.title + expect(json_response.first['id']).to eq merge_request_closed.id + end + end - expect_response_ordered_exactly(*merge_requests.sort_by { |mr| mr['created_at'] }) - end - end + describe "GET /groups/:id/merge_requests" do + let!(:group) { create(:group, :public) } + let!(:project) { create(:project, :public, :repository, creator: user, namespace: group, only_allow_merge_if_pipeline_succeeds: false) } + let(:endpoint_path) { "/groups/#{group.id}/merge_requests" } - context 'source_branch param' do - it 'returns merge requests with the given source branch' do - get api('/merge_requests', user), source_branch: merge_request_closed.source_branch, state: 'all' + before do + group.add_reporter(user) + end - expect_response_contain_exactly(merge_request_closed, merge_request_merged) - end - end + it_behaves_like 'merge requests list' - context 'target_branch param' do - it 'returns merge requests with the given target branch' do - get api('/merge_requests', user), target_branch: merge_request_closed.target_branch, state: 'all' + context 'when have subgroups', :nested_groups do + let!(:group) { create(:group, :public) } + let!(:subgroup) { create(:group, parent: group) } + let!(:project) { create(:project, :public, :repository, creator: user, namespace: subgroup, only_allow_merge_if_pipeline_succeeds: false) } - expect_response_contain_exactly(merge_request_closed, merge_request_merged) - end - end + it_behaves_like 'merge requests list' end end @@ -671,12 +500,14 @@ describe API::MergeRequests do target_branch: 'master', author: user, labels: 'label, label2', - milestone_id: milestone.id + milestone_id: milestone.id, + squash: true expect(response).to have_gitlab_http_status(201) expect(json_response['title']).to eq('Test merge_request') expect(json_response['labels']).to eq(%w(label label2)) expect(json_response['milestone']['id']).to eq(milestone.id) + expect(json_response['squash']).to be_truthy expect(json_response['force_remove_source_branch']).to be_falsy end @@ -965,6 +796,14 @@ describe API::MergeRequests do expect(response).to have_gitlab_http_status(200) end + it "updates the MR's squash attribute" do + expect do + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), squash: true + end.to change { merge_request.reload.squash } + + expect(response).to have_gitlab_http_status(200) + end + it "enables merge when pipeline succeeds if the pipeline is active" do allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(pipeline) allow(pipeline).to receive(:active?).and_return(true) @@ -1029,6 +868,13 @@ describe API::MergeRequests do expect(json_response['milestone']['id']).to eq(milestone.id) end + it "updates squash and returns merge_request" do + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), squash: true + + expect(response).to have_gitlab_http_status(200) + expect(json_response['squash']).to be_truthy + end + it "returns merge_request with renamed target_branch" do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), target_branch: "wiki" expect(response).to have_gitlab_http_status(200) diff --git a/spec/requests/api/v3/award_emoji_spec.rb b/spec/requests/api/v3/award_emoji_spec.rb deleted file mode 100644 index 6dc430676b0..00000000000 --- a/spec/requests/api/v3/award_emoji_spec.rb +++ /dev/null @@ -1,297 +0,0 @@ -require 'spec_helper' - -describe API::V3::AwardEmoji do - set(:user) { create(:user) } - set(:project) { create(:project) } - set(:issue) { create(:issue, project: project) } - set(:award_emoji) { create(:award_emoji, awardable: issue, user: user) } - let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } - let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) } - set(:note) { create(:note, project: project, noteable: issue) } - - before { project.add_master(user) } - - describe "GET /projects/:id/awardable/:awardable_id/award_emoji" do - context 'on an issue' do - it "returns an array of award_emoji" do - get v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['name']).to eq(award_emoji.name) - end - - it "returns a 404 error when issue id not found" do - get v3_api("/projects/#{project.id}/issues/12345/award_emoji", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'on a merge request' do - it "returns an array of award_emoji" do - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.first['name']).to eq(downvote.name) - end - end - - context 'on a snippet' do - let(:snippet) { create(:project_snippet, :public, project: project) } - let!(:award) { create(:award_emoji, awardable: snippet) } - - it 'returns the awarded emoji' do - get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['name']).to eq(award.name) - end - end - - context 'when the user has no access' do - it 'returns a status code 404' do - user1 = create(:user) - - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user1) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe 'GET /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji' do - let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') } - - it 'returns an array of award emoji' do - get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['name']).to eq(rocket.name) - end - end - - describe "GET /projects/:id/awardable/:awardable_id/award_emoji/:award_id" do - context 'on an issue' do - it "returns the award emoji" do - get v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['name']).to eq(award_emoji.name) - expect(json_response['awardable_id']).to eq(issue.id) - expect(json_response['awardable_type']).to eq("Issue") - end - - it "returns a 404 error if the award is not found" do - get v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'on a merge request' do - it 'returns the award emoji' do - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['name']).to eq(downvote.name) - expect(json_response['awardable_id']).to eq(merge_request.id) - expect(json_response['awardable_type']).to eq("MergeRequest") - end - end - - context 'on a snippet' do - let(:snippet) { create(:project_snippet, :public, project: project) } - let!(:award) { create(:award_emoji, awardable: snippet) } - - it 'returns the awarded emoji' do - get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['name']).to eq(award.name) - expect(json_response['awardable_id']).to eq(snippet.id) - expect(json_response['awardable_type']).to eq("Snippet") - end - end - - context 'when the user has no access' do - it 'returns a status code 404' do - user1 = create(:user) - - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user1) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe 'GET /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji/:award_id' do - let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') } - - it 'returns an award emoji' do - get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).not_to be_an Array - expect(json_response['name']).to eq(rocket.name) - end - end - - describe "POST /projects/:id/awardable/:awardable_id/award_emoji" do - let(:issue2) { create(:issue, project: project, author: user) } - - context "on an issue" do - it "creates a new award emoji" do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'blowfish' - - expect(response).to have_gitlab_http_status(201) - expect(json_response['name']).to eq('blowfish') - expect(json_response['user']['username']).to eq(user.username) - end - - it "returns a 400 bad request error if the name is not given" do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user) - - expect(response).to have_gitlab_http_status(400) - end - - it "returns a 401 unauthorized error if the user is not authenticated" do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji"), name: 'thumbsup' - - expect(response).to have_gitlab_http_status(401) - end - - it "returns a 404 error if the user authored issue" do - post v3_api("/projects/#{project.id}/issues/#{issue2.id}/award_emoji", user), name: 'thumbsup' - - expect(response).to have_gitlab_http_status(404) - end - - it "normalizes +1 as thumbsup award" do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: '+1' - - expect(issue.award_emoji.last.name).to eq("thumbsup") - end - - context 'when the emoji already has been awarded' do - it 'returns a 404 status code' do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup' - post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup' - - expect(response).to have_gitlab_http_status(404) - expect(json_response["message"]).to match("has already been taken") - end - end - end - - context 'on a snippet' do - it 'creates a new award emoji' do - snippet = create(:project_snippet, :public, project: project) - - post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user), name: 'blowfish' - - expect(response).to have_gitlab_http_status(201) - expect(json_response['name']).to eq('blowfish') - expect(json_response['user']['username']).to eq(user.username) - end - end - end - - describe "POST /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji" do - let(:note2) { create(:note, project: project, noteable: issue, author: user) } - - it 'creates a new award emoji' do - expect do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket' - end.to change { note.award_emoji.count }.from(0).to(1) - - expect(response).to have_gitlab_http_status(201) - expect(json_response['user']['username']).to eq(user.username) - end - - it "it returns 404 error when user authored note" do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note2.id}/award_emoji", user), name: 'thumbsup' - - expect(response).to have_gitlab_http_status(404) - end - - it "normalizes +1 as thumbsup award" do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: '+1' - - expect(note.award_emoji.last.name).to eq("thumbsup") - end - - context 'when the emoji already has been awarded' do - it 'returns a 404 status code' do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket' - post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket' - - expect(response).to have_gitlab_http_status(404) - expect(json_response["message"]).to match("has already been taken") - end - end - end - - describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_id' do - context 'when the awardable is an Issue' do - it 'deletes the award' do - expect do - delete v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user) - - expect(response).to have_gitlab_http_status(200) - end.to change { issue.award_emoji.count }.from(1).to(0) - end - - it 'returns a 404 error when the award emoji can not be found' do - delete v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'when the awardable is a Merge Request' do - it 'deletes the award' do - expect do - delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user) - - expect(response).to have_gitlab_http_status(200) - end.to change { merge_request.award_emoji.count }.from(1).to(0) - end - - it 'returns a 404 error when note id not found' do - delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes/12345", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'when the awardable is a Snippet' do - let(:snippet) { create(:project_snippet, :public, project: project) } - let!(:award) { create(:award_emoji, awardable: snippet, user: user) } - - it 'deletes the award' do - expect do - delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user) - - expect(response).to have_gitlab_http_status(200) - end.to change { snippet.award_emoji.count }.from(1).to(0) - end - end - end - - describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_emoji_id' do - let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket', user: user) } - - it 'deletes the award' do - expect do - delete v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user) - - expect(response).to have_gitlab_http_status(200) - end.to change { note.award_emoji.count }.from(1).to(0) - end - end -end diff --git a/spec/requests/api/v3/boards_spec.rb b/spec/requests/api/v3/boards_spec.rb deleted file mode 100644 index dde4f096193..00000000000 --- a/spec/requests/api/v3/boards_spec.rb +++ /dev/null @@ -1,114 +0,0 @@ -require 'spec_helper' - -describe API::V3::Boards do - set(:user) { create(:user) } - set(:guest) { create(:user) } - set(:non_member) { create(:user) } - set(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) } - - set(:dev_label) do - create(:label, title: 'Development', color: '#FFAABB', project: project) - end - - set(:test_label) do - create(:label, title: 'Testing', color: '#FFAACC', project: project) - end - - set(:dev_list) do - create(:list, label: dev_label, position: 1) - end - - set(:test_list) do - create(:list, label: test_label, position: 2) - end - - set(:board) do - create(:board, project: project, lists: [dev_list, test_list]) - end - - before do - project.add_reporter(user) - project.add_guest(guest) - end - - describe "GET /projects/:id/boards" do - let(:base_url) { "/projects/#{project.id}/boards" } - - context "when unauthenticated" do - it "returns authentication error" do - get v3_api(base_url) - - expect(response).to have_gitlab_http_status(401) - end - end - - context "when authenticated" do - it "returns the project issue board" do - get v3_api(base_url, user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(board.id) - expect(json_response.first['lists']).to be_an Array - expect(json_response.first['lists'].length).to eq(2) - expect(json_response.first['lists'].last).to have_key('position') - end - end - end - - describe "GET /projects/:id/boards/:board_id/lists" do - let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } - - it 'returns issue board lists' do - get v3_api(base_url, user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - expect(json_response.first['label']['name']).to eq(dev_label.title) - end - - it 'returns 404 if board not found' do - get v3_api("/projects/#{project.id}/boards/22343/lists", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe "DELETE /projects/:id/board/lists/:list_id" do - let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } - - it "rejects a non member from deleting a list" do - delete v3_api("#{base_url}/#{dev_list.id}", non_member) - - expect(response).to have_gitlab_http_status(403) - end - - it "rejects a user with guest role from deleting a list" do - delete v3_api("#{base_url}/#{dev_list.id}", guest) - - expect(response).to have_gitlab_http_status(403) - end - - it "returns 404 error if list id not found" do - delete v3_api("#{base_url}/44444", user) - - expect(response).to have_gitlab_http_status(404) - end - - context "when the user is project owner" do - set(:owner) { create(:user) } - - before do - project.update(namespace: owner.namespace) - end - - it "deletes the list if an admin requests it" do - delete v3_api("#{base_url}/#{dev_list.id}", owner) - - expect(response).to have_gitlab_http_status(200) - end - end - end -end diff --git a/spec/requests/api/v3/branches_spec.rb b/spec/requests/api/v3/branches_spec.rb deleted file mode 100644 index 1e038595a1f..00000000000 --- a/spec/requests/api/v3/branches_spec.rb +++ /dev/null @@ -1,120 +0,0 @@ -require 'spec_helper' -require 'mime/types' - -describe API::V3::Branches do - set(:user) { create(:user) } - set(:user2) { create(:user) } - set(:project) { create(:project, :repository, creator: user) } - set(:master) { create(:project_member, :master, user: user, project: project) } - set(:guest) { create(:project_member, :guest, user: user2, project: project) } - let!(:branch_name) { 'feature' } - let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } - let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") } - - describe "GET /projects/:id/repository/branches" do - it "returns an array of project branches" do - project.repository.expire_all_method_caches - - get v3_api("/projects/#{project.id}/repository/branches", user), per_page: 100 - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - branch_names = json_response.map { |x| x['name'] } - expect(branch_names).to match_array(project.repository.branch_names) - end - end - - describe "DELETE /projects/:id/repository/branches/:branch" do - before do - allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true) - end - - it "removes branch" do - delete v3_api("/projects/#{project.id}/repository/branches/#{branch_name}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['branch_name']).to eq(branch_name) - end - - it "removes a branch with dots in the branch name" do - delete v3_api("/projects/#{project.id}/repository/branches/with.1.2.3", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['branch_name']).to eq("with.1.2.3") - end - - it 'returns 404 if branch not exists' do - delete v3_api("/projects/#{project.id}/repository/branches/foobar", user) - expect(response).to have_gitlab_http_status(404) - end - end - - describe "DELETE /projects/:id/repository/merged_branches" do - before do - allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true) - end - - it 'returns 200' do - delete v3_api("/projects/#{project.id}/repository/merged_branches", user) - - expect(response).to have_gitlab_http_status(200) - end - - it 'returns a 403 error if guest' do - delete v3_api("/projects/#{project.id}/repository/merged_branches", user2) - - expect(response).to have_gitlab_http_status(403) - end - end - - describe "POST /projects/:id/repository/branches" do - it "creates a new branch" do - post v3_api("/projects/#{project.id}/repository/branches", user), - branch_name: 'feature1', - ref: branch_sha - - expect(response).to have_gitlab_http_status(201) - - expect(json_response['name']).to eq('feature1') - expect(json_response['commit']['id']).to eq(branch_sha) - end - - it "denies for user without push access" do - post v3_api("/projects/#{project.id}/repository/branches", user2), - branch_name: branch_name, - ref: branch_sha - expect(response).to have_gitlab_http_status(403) - end - - it 'returns 400 if branch name is invalid' do - post v3_api("/projects/#{project.id}/repository/branches", user), - branch_name: 'new design', - ref: branch_sha - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']).to eq('Branch name is invalid') - end - - it 'returns 400 if branch already exists' do - post v3_api("/projects/#{project.id}/repository/branches", user), - branch_name: 'new_design1', - ref: branch_sha - expect(response).to have_gitlab_http_status(201) - - post v3_api("/projects/#{project.id}/repository/branches", user), - branch_name: 'new_design1', - ref: branch_sha - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']).to eq('Branch already exists') - end - - it 'returns 400 if ref name is invalid' do - post v3_api("/projects/#{project.id}/repository/branches", user), - branch_name: 'new_design3', - ref: 'foo' - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']).to eq('Invalid reference name') - end - end -end diff --git a/spec/requests/api/v3/broadcast_messages_spec.rb b/spec/requests/api/v3/broadcast_messages_spec.rb deleted file mode 100644 index d9641011491..00000000000 --- a/spec/requests/api/v3/broadcast_messages_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -require 'spec_helper' - -describe API::V3::BroadcastMessages do - set(:user) { create(:user) } - set(:admin) { create(:admin) } - - describe 'DELETE /broadcast_messages/:id' do - set(:message) { create(:broadcast_message) } - - it 'returns a 401 for anonymous users' do - delete v3_api("/broadcast_messages/#{message.id}"), - attributes_for(:broadcast_message) - - expect(response).to have_gitlab_http_status(401) - end - - it 'returns a 403 for users' do - delete v3_api("/broadcast_messages/#{message.id}", user), - attributes_for(:broadcast_message) - - expect(response).to have_gitlab_http_status(403) - end - - it 'deletes the broadcast message for admins' do - expect do - delete v3_api("/broadcast_messages/#{message.id}", admin) - - expect(response).to have_gitlab_http_status(200) - end.to change { BroadcastMessage.count }.by(-1) - end - end -end diff --git a/spec/requests/api/v3/builds_spec.rb b/spec/requests/api/v3/builds_spec.rb deleted file mode 100644 index 485d7c2cc43..00000000000 --- a/spec/requests/api/v3/builds_spec.rb +++ /dev/null @@ -1,550 +0,0 @@ -require 'spec_helper' - -describe API::V3::Builds do - set(:user) { create(:user) } - let(:api_user) { user } - set(:project) { create(:project, :repository, creator: user, public_builds: false) } - let!(:developer) { create(:project_member, :developer, user: user, project: project) } - let(:reporter) { create(:project_member, :reporter, project: project) } - let(:guest) { create(:project_member, :guest, project: project) } - let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) } - let(:build) { create(:ci_build, pipeline: pipeline) } - - describe 'GET /projects/:id/builds ' do - let(:query) { '' } - - before do |example| - build - - create(:ci_build, :skipped, pipeline: pipeline) - - unless example.metadata[:skip_before_request] - get v3_api("/projects/#{project.id}/builds?#{query}", api_user) - end - end - - context 'authorized user' do - it 'returns project builds' do - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - end - - it 'returns correct values' do - expect(json_response).not_to be_empty - expect(json_response.first['commit']['id']).to eq project.commit.id - end - - it 'returns pipeline data' do - json_build = json_response.first - expect(json_build['pipeline']).not_to be_empty - expect(json_build['pipeline']['id']).to eq build.pipeline.id - expect(json_build['pipeline']['ref']).to eq build.pipeline.ref - expect(json_build['pipeline']['sha']).to eq build.pipeline.sha - expect(json_build['pipeline']['status']).to eq build.pipeline.status - end - - it 'avoids N+1 queries', :skip_before_request do - first_build = create(:ci_build, :artifacts, pipeline: pipeline) - first_build.runner = create(:ci_runner) - first_build.user = create(:user) - first_build.save - - control_count = ActiveRecord::QueryRecorder.new { go }.count - - second_pipeline = create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) - second_build = create(:ci_build, :artifacts, pipeline: second_pipeline) - second_build.runner = create(:ci_runner) - second_build.user = create(:user) - second_build.save - - expect { go }.not_to exceed_query_limit(control_count) - end - - context 'filter project with one scope element' do - let(:query) { 'scope=pending' } - - it do - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - end - end - - context 'filter project with scope skipped' do - let(:query) { 'scope=skipped' } - let(:json_build) { json_response.first } - - it 'return builds with status skipped' do - expect(response).to have_gitlab_http_status 200 - expect(json_response).to be_an Array - expect(json_response.length).to eq 1 - expect(json_build['status']).to eq 'skipped' - end - end - - context 'filter project with array of scope elements' do - let(:query) { 'scope[0]=pending&scope[1]=running' } - - it do - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - end - end - - context 'respond 400 when scope contains invalid state' do - let(:query) { 'scope[0]=pending&scope[1]=unknown_status' } - - it { expect(response).to have_gitlab_http_status(400) } - end - end - - context 'unauthorized user' do - let(:api_user) { nil } - - it 'does not return project builds' do - expect(response).to have_gitlab_http_status(401) - end - end - - def go - get v3_api("/projects/#{project.id}/builds?#{query}", api_user) - end - end - - describe 'GET /projects/:id/repository/commits/:sha/builds' do - before do - build - end - - context 'when commit does not exist in repository' do - before do - get v3_api("/projects/#{project.id}/repository/commits/1a271fd1/builds", api_user) - end - - it 'responds with 404' do - expect(response).to have_gitlab_http_status(404) - end - end - - context 'when commit exists in repository' do - context 'when user is authorized' do - context 'when pipeline has jobs' do - before do - create(:ci_pipeline, project: project, sha: project.commit.id) - create(:ci_build, pipeline: pipeline) - create(:ci_build) - - get v3_api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", api_user) - end - - it 'returns project jobs for specific commit' do - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.size).to eq 2 - end - - it 'returns pipeline data' do - json_build = json_response.first - expect(json_build['pipeline']).not_to be_empty - expect(json_build['pipeline']['id']).to eq build.pipeline.id - expect(json_build['pipeline']['ref']).to eq build.pipeline.ref - expect(json_build['pipeline']['sha']).to eq build.pipeline.sha - expect(json_build['pipeline']['status']).to eq build.pipeline.status - end - end - - context 'when pipeline has no jobs' do - before do - branch_head = project.commit('feature').id - get v3_api("/projects/#{project.id}/repository/commits/#{branch_head}/builds", api_user) - end - - it 'returns an empty array' do - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response).to be_empty - end - end - end - - context 'when user is not authorized' do - before do - create(:ci_pipeline, project: project, sha: project.commit.id) - create(:ci_build, pipeline: pipeline) - - get v3_api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", nil) - end - - it 'does not return project jobs' do - expect(response).to have_gitlab_http_status(401) - expect(json_response.except('message')).to be_empty - end - end - end - end - - describe 'GET /projects/:id/builds/:build_id' do - before do - get v3_api("/projects/#{project.id}/builds/#{build.id}", api_user) - end - - context 'authorized user' do - it 'returns specific job data' do - expect(response).to have_gitlab_http_status(200) - expect(json_response['name']).to eq('test') - end - - it 'returns pipeline data' do - json_build = json_response - expect(json_build['pipeline']).not_to be_empty - expect(json_build['pipeline']['id']).to eq build.pipeline.id - expect(json_build['pipeline']['ref']).to eq build.pipeline.ref - expect(json_build['pipeline']['sha']).to eq build.pipeline.sha - expect(json_build['pipeline']['status']).to eq build.pipeline.status - end - end - - context 'unauthorized user' do - let(:api_user) { nil } - - it 'does not return specific job data' do - expect(response).to have_gitlab_http_status(401) - end - end - end - - describe 'GET /projects/:id/builds/:build_id/artifacts' do - before do - stub_artifacts_object_storage - get v3_api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) - end - - context 'job with artifacts' do - context 'when artifacts are stored locally' do - let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } - - context 'authorized user' do - let(:download_headers) do - { 'Content-Transfer-Encoding' => 'binary', - 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' } - end - - it 'returns specific job artifacts' do - expect(response).to have_http_status(200) - expect(response.headers.to_h).to include(download_headers) - expect(response.body).to match_file(build.artifacts_file.file.file) - end - end - end - - context 'when artifacts are stored remotely' do - let(:build) { create(:ci_build, pipeline: pipeline) } - let!(:artifact) { create(:ci_job_artifact, :archive, :remote_store, job: build) } - - it 'returns location redirect' do - get v3_api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) - - expect(response).to have_gitlab_http_status(302) - end - end - - context 'unauthorized user' do - let(:api_user) { nil } - - it 'does not return specific job artifacts' do - expect(response).to have_gitlab_http_status(401) - end - end - end - - it 'does not return job artifacts if not uploaded' do - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do - let(:api_user) { reporter.user } - let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } - - before do - stub_artifacts_object_storage - build.success - end - - def path_for_ref(ref = pipeline.ref, job = build.name) - v3_api("/projects/#{project.id}/builds/artifacts/#{ref}/download?job=#{job}", api_user) - end - - context 'when not logged in' do - let(:api_user) { nil } - - before do - get path_for_ref - end - - it 'gives 401' do - expect(response).to have_gitlab_http_status(401) - end - end - - context 'when logging as guest' do - let(:api_user) { guest.user } - - before do - get path_for_ref - end - - it 'gives 403' do - expect(response).to have_gitlab_http_status(403) - end - end - - context 'non-existing job' do - shared_examples 'not found' do - it { expect(response).to have_gitlab_http_status(:not_found) } - end - - context 'has no such ref' do - before do - get path_for_ref('TAIL', build.name) - end - - it_behaves_like 'not found' - end - - context 'has no such job' do - before do - get path_for_ref(pipeline.ref, 'NOBUILD') - end - - it_behaves_like 'not found' - end - end - - context 'find proper job' do - shared_examples 'a valid file' do - context 'when artifacts are stored locally' do - let(:download_headers) do - { 'Content-Transfer-Encoding' => 'binary', - 'Content-Disposition' => - "attachment; filename=#{build.artifacts_file.filename}" } - end - - it { expect(response).to have_http_status(200) } - it { expect(response.headers.to_h).to include(download_headers) } - end - - context 'when artifacts are stored remotely' do - let(:build) { create(:ci_build, pipeline: pipeline) } - let!(:artifact) { create(:ci_job_artifact, :archive, :remote_store, job: build) } - - before do - build.reload - - get v3_api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) - end - - it 'returns location redirect' do - expect(response).to have_http_status(302) - end - end - end - - context 'with regular branch' do - before do - pipeline.reload - pipeline.update(ref: 'master', - sha: project.commit('master').sha) - - get path_for_ref('master') - end - - it_behaves_like 'a valid file' - end - - context 'with branch name containing slash' do - before do - pipeline.reload - pipeline.update(ref: 'improve/awesome', - sha: project.commit('improve/awesome').sha) - end - - before do - get path_for_ref('improve/awesome') - end - - it_behaves_like 'a valid file' - end - end - end - - describe 'GET /projects/:id/builds/:build_id/trace' do - let(:build) { create(:ci_build, :trace_live, pipeline: pipeline) } - - before do - get v3_api("/projects/#{project.id}/builds/#{build.id}/trace", api_user) - end - - context 'authorized user' do - it 'returns specific job trace' do - expect(response).to have_gitlab_http_status(200) - expect(response.body).to eq(build.trace.raw) - end - end - - context 'unauthorized user' do - let(:api_user) { nil } - - it 'does not return specific job trace' do - expect(response).to have_gitlab_http_status(401) - end - end - end - - describe 'POST /projects/:id/builds/:build_id/cancel' do - before do - post v3_api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user) - end - - context 'authorized user' do - context 'user with :update_build persmission' do - it 'cancels running or pending job' do - expect(response).to have_gitlab_http_status(201) - expect(project.builds.first.status).to eq('canceled') - end - end - - context 'user without :update_build permission' do - let(:api_user) { reporter.user } - - it 'does not cancel job' do - expect(response).to have_gitlab_http_status(403) - end - end - end - - context 'unauthorized user' do - let(:api_user) { nil } - - it 'does not cancel job' do - expect(response).to have_gitlab_http_status(401) - end - end - end - - describe 'POST /projects/:id/builds/:build_id/retry' do - let(:build) { create(:ci_build, :canceled, pipeline: pipeline) } - - before do - post v3_api("/projects/#{project.id}/builds/#{build.id}/retry", api_user) - end - - context 'authorized user' do - context 'user with :update_build permission' do - it 'retries non-running job' do - expect(response).to have_gitlab_http_status(201) - expect(project.builds.first.status).to eq('canceled') - expect(json_response['status']).to eq('pending') - end - end - - context 'user without :update_build permission' do - let(:api_user) { reporter.user } - - it 'does not retry job' do - expect(response).to have_gitlab_http_status(403) - end - end - end - - context 'unauthorized user' do - let(:api_user) { nil } - - it 'does not retry job' do - expect(response).to have_gitlab_http_status(401) - end - end - end - - describe 'POST /projects/:id/builds/:build_id/erase' do - before do - project.add_master(user) - - post v3_api("/projects/#{project.id}/builds/#{build.id}/erase", user) - end - - context 'job is erasable' do - let(:build) { create(:ci_build, :trace_artifact, :artifacts, :success, project: project, pipeline: pipeline) } - - it 'erases job content' do - expect(response.status).to eq 201 - expect(build).not_to have_trace - expect(build.artifacts_file.exists?).to be_falsy - expect(build.artifacts_metadata.exists?).to be_falsy - end - - it 'updates job' do - expect(build.reload.erased_at).to be_truthy - expect(build.reload.erased_by).to eq user - end - end - - context 'job is not erasable' do - let(:build) { create(:ci_build, :trace_live, project: project, pipeline: pipeline) } - - it 'responds with forbidden' do - expect(response.status).to eq 403 - end - end - end - - describe 'POST /projects/:id/builds/:build_id/artifacts/keep' do - before do - post v3_api("/projects/#{project.id}/builds/#{build.id}/artifacts/keep", user) - end - - context 'artifacts did not expire' do - let(:build) do - create(:ci_build, :trace_artifact, :artifacts, :success, - project: project, pipeline: pipeline, artifacts_expire_at: Time.now + 7.days) - end - - it 'keeps artifacts' do - expect(response.status).to eq 200 - expect(build.reload.artifacts_expire_at).to be_nil - end - end - - context 'no artifacts' do - let(:build) { create(:ci_build, project: project, pipeline: pipeline) } - - it 'responds with not found' do - expect(response.status).to eq 404 - end - end - end - - describe 'POST /projects/:id/builds/:build_id/play' do - before do - post v3_api("/projects/#{project.id}/builds/#{build.id}/play", user) - end - - context 'on an playable job' do - let(:build) { create(:ci_build, :manual, project: project, pipeline: pipeline) } - - it 'plays the job' do - expect(response).to have_gitlab_http_status 200 - expect(json_response['user']['id']).to eq(user.id) - expect(json_response['id']).to eq(build.id) - end - end - - context 'on a non-playable job' do - it 'returns a status code 400, Bad Request' do - expect(response).to have_gitlab_http_status 400 - expect(response.body).to match("Unplayable Job") - end - end - end -end diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb deleted file mode 100644 index 9ef3b859001..00000000000 --- a/spec/requests/api/v3/commits_spec.rb +++ /dev/null @@ -1,603 +0,0 @@ -require 'spec_helper' -require 'mime/types' - -describe API::V3::Commits do - let(:user) { create(:user) } - let(:user2) { create(:user) } - let!(:project) { create(:project, :repository, creator: user, namespace: user.namespace) } - let!(:guest) { create(:project_member, :guest, user: user2, project: project) } - let!(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') } - let!(:another_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'another comment on a commit') } - - before { project.add_reporter(user) } - - describe "List repository commits" do - context "authorized user" do - before { project.add_reporter(user2) } - - it "returns project commits" do - commit = project.repository.commit - get v3_api("/projects/#{project.id}/repository/commits", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['id']).to eq(commit.id) - expect(json_response.first['committer_name']).to eq(commit.committer_name) - expect(json_response.first['committer_email']).to eq(commit.committer_email) - end - end - - context "unauthorized user" do - it "does not return project commits" do - get v3_api("/projects/#{project.id}/repository/commits") - expect(response).to have_gitlab_http_status(401) - end - end - - context "since optional parameter" do - it "returns project commits since provided parameter" do - commits = project.repository.commits("master", limit: 2) - since = commits.second.created_at - - get v3_api("/projects/#{project.id}/repository/commits?since=#{since.utc.iso8601}", user) - - expect(json_response.size).to eq 2 - expect(json_response.first["id"]).to eq(commits.first.id) - expect(json_response.second["id"]).to eq(commits.second.id) - end - end - - context "until optional parameter" do - it "returns project commits until provided parameter" do - commits = project.repository.commits("master", limit: 20) - before = commits.second.created_at - - get v3_api("/projects/#{project.id}/repository/commits?until=#{before.utc.iso8601}", user) - - if commits.size == 20 - expect(json_response.size).to eq(20) - else - expect(json_response.size).to eq(commits.size - 1) - end - - expect(json_response.first["id"]).to eq(commits.second.id) - expect(json_response.second["id"]).to eq(commits.third.id) - end - end - - context "invalid xmlschema date parameters" do - it "returns an invalid parameter error message" do - get v3_api("/projects/#{project.id}/repository/commits?since=invalid-date", user) - - expect(response).to have_gitlab_http_status(400) - expect(json_response['error']).to eq('since is invalid') - end - end - - context "path optional parameter" do - it "returns project commits matching provided path parameter" do - path = 'files/ruby/popen.rb' - - get v3_api("/projects/#{project.id}/repository/commits?path=#{path}", user) - - expect(json_response.size).to eq(3) - expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") - end - end - end - - describe "POST /projects/:id/repository/commits" do - let!(:url) { "/projects/#{project.id}/repository/commits" } - - it 'returns a 403 unauthorized for user without permissions' do - post v3_api(url, user2) - - expect(response).to have_gitlab_http_status(403) - end - - it 'returns a 400 bad request if no params are given' do - post v3_api(url, user) - - expect(response).to have_gitlab_http_status(400) - end - - describe 'create' do - let(:message) { 'Created file' } - let!(:invalid_c_params) do - { - branch_name: 'master', - commit_message: message, - actions: [ - { - action: 'create', - file_path: 'files/ruby/popen.rb', - content: 'puts 8' - } - ] - } - end - let!(:valid_c_params) do - { - branch_name: 'master', - commit_message: message, - actions: [ - { - action: 'create', - file_path: 'foo/bar/baz.txt', - content: 'puts 8' - } - ] - } - end - - it 'a new file in project repo' do - post v3_api(url, user), valid_c_params - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq(message) - expect(json_response['committer_name']).to eq(user.name) - expect(json_response['committer_email']).to eq(user.email) - end - - it 'returns a 400 bad request if file exists' do - post v3_api(url, user), invalid_c_params - - expect(response).to have_gitlab_http_status(400) - end - - context 'with project path containing a dot in URL' do - let!(:user) { create(:user, username: 'foo.bar') } - let(:url) { "/projects/#{CGI.escape(project.full_path)}/repository/commits" } - - it 'a new file in project repo' do - post v3_api(url, user), valid_c_params - - expect(response).to have_gitlab_http_status(201) - end - end - end - - describe 'delete' do - let(:message) { 'Deleted file' } - let!(:invalid_d_params) do - { - branch_name: 'markdown', - commit_message: message, - actions: [ - { - action: 'delete', - file_path: 'doc/api/projects.md' - } - ] - } - end - let!(:valid_d_params) do - { - branch_name: 'markdown', - commit_message: message, - actions: [ - { - action: 'delete', - file_path: 'doc/api/users.md' - } - ] - } - end - - it 'an existing file in project repo' do - post v3_api(url, user), valid_d_params - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq(message) - end - - it 'returns a 400 bad request if file does not exist' do - post v3_api(url, user), invalid_d_params - - expect(response).to have_gitlab_http_status(400) - end - end - - describe 'move' do - let(:message) { 'Moved file' } - let!(:invalid_m_params) do - { - branch_name: 'feature', - commit_message: message, - actions: [ - { - action: 'move', - file_path: 'CHANGELOG', - previous_path: 'VERSION', - content: '6.7.0.pre' - } - ] - } - end - let!(:valid_m_params) do - { - branch_name: 'feature', - commit_message: message, - actions: [ - { - action: 'move', - file_path: 'VERSION.txt', - previous_path: 'VERSION', - content: '6.7.0.pre' - } - ] - } - end - - it 'an existing file in project repo' do - post v3_api(url, user), valid_m_params - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq(message) - end - - it 'returns a 400 bad request if file does not exist' do - post v3_api(url, user), invalid_m_params - - expect(response).to have_gitlab_http_status(400) - end - end - - describe 'update' do - let(:message) { 'Updated file' } - let!(:invalid_u_params) do - { - branch_name: 'master', - commit_message: message, - actions: [ - { - action: 'update', - file_path: 'foo/bar.baz', - content: 'puts 8' - } - ] - } - end - let!(:valid_u_params) do - { - branch_name: 'master', - commit_message: message, - actions: [ - { - action: 'update', - file_path: 'files/ruby/popen.rb', - content: 'puts 8' - } - ] - } - end - - it 'an existing file in project repo' do - post v3_api(url, user), valid_u_params - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq(message) - end - - it 'returns a 400 bad request if file does not exist' do - post v3_api(url, user), invalid_u_params - - expect(response).to have_gitlab_http_status(400) - end - end - - context "multiple operations" do - let(:message) { 'Multiple actions' } - let!(:invalid_mo_params) do - { - branch_name: 'master', - commit_message: message, - actions: [ - { - action: 'create', - file_path: 'files/ruby/popen.rb', - content: 'puts 8' - }, - { - action: 'delete', - file_path: 'doc/v3_api/projects.md' - }, - { - action: 'move', - file_path: 'CHANGELOG', - previous_path: 'VERSION', - content: '6.7.0.pre' - }, - { - action: 'update', - file_path: 'foo/bar.baz', - content: 'puts 8' - } - ] - } - end - let!(:valid_mo_params) do - { - branch_name: 'master', - commit_message: message, - actions: [ - { - action: 'create', - file_path: 'foo/bar/baz.txt', - content: 'puts 8' - }, - { - action: 'delete', - file_path: 'Gemfile.zip' - }, - { - action: 'move', - file_path: 'VERSION.txt', - previous_path: 'VERSION', - content: '6.7.0.pre' - }, - { - action: 'update', - file_path: 'files/ruby/popen.rb', - content: 'puts 8' - } - ] - } - end - - it 'are commited as one in project repo' do - post v3_api(url, user), valid_mo_params - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq(message) - end - - it 'return a 400 bad request if there are any issues' do - post v3_api(url, user), invalid_mo_params - - expect(response).to have_gitlab_http_status(400) - end - end - end - - describe "Get a single commit" do - context "authorized user" do - it "returns a commit by sha" do - get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['id']).to eq(project.repository.commit.id) - expect(json_response['title']).to eq(project.repository.commit.title) - expect(json_response['stats']['additions']).to eq(project.repository.commit.stats.additions) - expect(json_response['stats']['deletions']).to eq(project.repository.commit.stats.deletions) - expect(json_response['stats']['total']).to eq(project.repository.commit.stats.total) - end - - it "returns a 404 error if not found" do - get v3_api("/projects/#{project.id}/repository/commits/invalid_sha", user) - expect(response).to have_gitlab_http_status(404) - end - - it "returns nil for commit without CI" do - get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['status']).to be_nil - end - - it "returns status for CI" do - pipeline = project.pipelines.create(source: :push, ref: 'master', sha: project.repository.commit.sha, protected: false) - pipeline.update(status: 'success') - - get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['status']).to eq(pipeline.status) - end - - it "returns status for CI when pipeline is created" do - project.pipelines.create(source: :push, ref: 'master', sha: project.repository.commit.sha, protected: false) - - get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['status']).to eq("created") - end - - context 'when stat param' do - let(:project_id) { project.id } - let(:commit_id) { project.repository.commit.id } - let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}" } - - it 'is not present return stats by default' do - get v3_api(route, user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to include 'stats' - end - - it "is false it does not include stats" do - get v3_api(route, user), stats: false - - expect(response).to have_gitlab_http_status(200) - expect(json_response).not_to include 'stats' - end - - it "is true it includes stats" do - get v3_api(route, user), stats: true - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to include 'stats' - end - end - end - - context "unauthorized user" do - it "does not return the selected commit" do - get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}") - expect(response).to have_gitlab_http_status(401) - end - end - end - - describe "Get the diff of a commit" do - context "authorized user" do - before { project.add_reporter(user2) } - - it "returns the diff of the selected commit" do - get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff", user) - expect(response).to have_gitlab_http_status(200) - - expect(json_response).to be_an Array - expect(json_response.length).to be >= 1 - expect(json_response.first.keys).to include "diff" - end - - it "returns a 404 error if invalid commit" do - get v3_api("/projects/#{project.id}/repository/commits/invalid_sha/diff", user) - expect(response).to have_gitlab_http_status(404) - end - end - - context "unauthorized user" do - it "does not return the diff of the selected commit" do - get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff") - expect(response).to have_gitlab_http_status(401) - end - end - end - - describe 'Get the comments of a commit' do - context 'authorized user' do - it 'returns merge_request comments' do - get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - expect(json_response.first['note']).to eq('a comment on a commit') - expect(json_response.first['author']['id']).to eq(user.id) - end - - it 'returns a 404 error if merge_request_id not found' do - get v3_api("/projects/#{project.id}/repository/commits/1234ab/comments", user) - expect(response).to have_gitlab_http_status(404) - end - end - - context 'unauthorized user' do - it 'does not return the diff of the selected commit' do - get v3_api("/projects/#{project.id}/repository/commits/1234ab/comments") - expect(response).to have_gitlab_http_status(401) - end - end - end - - describe 'POST :id/repository/commits/:sha/cherry_pick' do - let(:master_pickable_commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') } - - context 'authorized user' do - it 'cherry picks a commit' do - post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'master' - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq(master_pickable_commit.title) - expect(json_response['message']).to eq(master_pickable_commit.cherry_pick_message(user)) - expect(json_response['author_name']).to eq(master_pickable_commit.author_name) - expect(json_response['committer_name']).to eq(user.name) - end - - it 'returns 400 if commit is already included in the target branch' do - post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'markdown' - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']).to include('Sorry, we cannot cherry-pick this commit automatically.') - end - - it 'returns 400 if you are not allowed to push to the target branch' do - project.add_developer(user2) - protected_branch = create(:protected_branch, project: project, name: 'feature') - - post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user2), branch: protected_branch.name - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']).to eq('You are not allowed to push into this branch') - end - - it 'returns 400 for missing parameters' do - post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user) - - expect(response).to have_gitlab_http_status(400) - expect(json_response['error']).to eq('branch is missing') - end - - it 'returns 404 if commit is not found' do - post v3_api("/projects/#{project.id}/repository/commits/abcd0123/cherry_pick", user), branch: 'master' - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Commit Not Found') - end - - it 'returns 404 if branch is not found' do - post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'foo' - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Branch Not Found') - end - - it 'returns 400 for missing parameters' do - post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user) - - expect(response).to have_gitlab_http_status(400) - expect(json_response['error']).to eq('branch is missing') - end - end - - context 'unauthorized user' do - it 'does not cherry pick the commit' do - post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick"), branch: 'master' - - expect(response).to have_gitlab_http_status(401) - end - end - end - - describe 'Post comment to commit' do - context 'authorized user' do - it 'returns comment' do - post v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment' - expect(response).to have_gitlab_http_status(201) - expect(json_response['note']).to eq('My comment') - expect(json_response['path']).to be_nil - expect(json_response['line']).to be_nil - expect(json_response['line_type']).to be_nil - end - - it 'returns the inline comment' do - post v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment', path: project.repository.commit.raw_diffs.first.new_path, line: 1, line_type: 'new' - - expect(response).to have_gitlab_http_status(201) - expect(json_response['note']).to eq('My comment') - expect(json_response['path']).to eq(project.repository.commit.raw_diffs.first.new_path) - expect(json_response['line']).to eq(1) - expect(json_response['line_type']).to eq('new') - end - - it 'returns 400 if note is missing' do - post v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user) - expect(response).to have_gitlab_http_status(400) - end - - it 'returns 404 if note is attached to non existent commit' do - post v3_api("/projects/#{project.id}/repository/commits/1234ab/comments", user), note: 'My comment' - expect(response).to have_gitlab_http_status(404) - end - end - - context 'unauthorized user' do - it 'does not return the diff of the selected commit' do - post v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments") - expect(response).to have_gitlab_http_status(401) - end - end - end -end diff --git a/spec/requests/api/v3/deploy_keys_spec.rb b/spec/requests/api/v3/deploy_keys_spec.rb deleted file mode 100644 index 501af587ad4..00000000000 --- a/spec/requests/api/v3/deploy_keys_spec.rb +++ /dev/null @@ -1,179 +0,0 @@ -require 'spec_helper' - -describe API::V3::DeployKeys do - let(:user) { create(:user) } - let(:admin) { create(:admin) } - let(:project) { create(:project, creator_id: user.id) } - let(:project2) { create(:project, creator_id: user.id) } - let(:deploy_key) { create(:deploy_key, public: true) } - - let!(:deploy_keys_project) do - create(:deploy_keys_project, project: project, deploy_key: deploy_key) - end - - describe 'GET /deploy_keys' do - context 'when unauthenticated' do - it 'should return authentication error' do - get v3_api('/deploy_keys') - - expect(response.status).to eq(401) - end - end - - context 'when authenticated as non-admin user' do - it 'should return a 403 error' do - get v3_api('/deploy_keys', user) - - expect(response.status).to eq(403) - end - end - - context 'when authenticated as admin' do - it 'should return all deploy keys' do - get v3_api('/deploy_keys', admin) - - expect(response.status).to eq(200) - expect(json_response).to be_an Array - expect(json_response.first['id']).to eq(deploy_keys_project.deploy_key.id) - end - end - end - - %w(deploy_keys keys).each do |path| - describe "GET /projects/:id/#{path}" do - before { deploy_key } - - it 'should return array of ssh keys' do - get v3_api("/projects/#{project.id}/#{path}", admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['title']).to eq(deploy_key.title) - end - end - - describe "GET /projects/:id/#{path}/:key_id" do - it 'should return a single key' do - get v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}", admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq(deploy_key.title) - end - - it 'should return 404 Not Found with invalid ID' do - get v3_api("/projects/#{project.id}/#{path}/404", admin) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe "POST /projects/:id/deploy_keys" do - it 'should not create an invalid ssh key' do - post v3_api("/projects/#{project.id}/#{path}", admin), { title: 'invalid key' } - - expect(response).to have_gitlab_http_status(400) - expect(json_response['error']).to eq('key is missing') - end - - it 'should not create a key without title' do - post v3_api("/projects/#{project.id}/#{path}", admin), key: 'some key' - - expect(response).to have_gitlab_http_status(400) - expect(json_response['error']).to eq('title is missing') - end - - it 'should create new ssh key' do - key_attrs = attributes_for :another_key - - expect do - post v3_api("/projects/#{project.id}/#{path}", admin), key_attrs - end.to change { project.deploy_keys.count }.by(1) - end - - it 'returns an existing ssh key when attempting to add a duplicate' do - expect do - post v3_api("/projects/#{project.id}/#{path}", admin), { key: deploy_key.key, title: deploy_key.title } - end.not_to change { project.deploy_keys.count } - - expect(response).to have_gitlab_http_status(201) - end - - it 'joins an existing ssh key to a new project' do - expect do - post v3_api("/projects/#{project2.id}/#{path}", admin), { key: deploy_key.key, title: deploy_key.title } - end.to change { project2.deploy_keys.count }.by(1) - - expect(response).to have_gitlab_http_status(201) - end - - it 'accepts can_push parameter' do - key_attrs = attributes_for(:another_key).merge(can_push: true) - - post v3_api("/projects/#{project.id}/#{path}", admin), key_attrs - - expect(response).to have_gitlab_http_status(201) - expect(json_response['can_push']).to eq(true) - end - end - - describe "DELETE /projects/:id/#{path}/:key_id" do - before { deploy_key } - - it 'should delete existing key' do - expect do - delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}", admin) - end.to change { project.deploy_keys.count }.by(-1) - end - - it 'should return 404 Not Found with invalid ID' do - delete v3_api("/projects/#{project.id}/#{path}/404", admin) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe "POST /projects/:id/#{path}/:key_id/enable" do - let(:project2) { create(:project) } - - context 'when the user can admin the project' do - it 'enables the key' do - expect do - post v3_api("/projects/#{project2.id}/#{path}/#{deploy_key.id}/enable", admin) - end.to change { project2.deploy_keys.count }.from(0).to(1) - - expect(response).to have_gitlab_http_status(201) - expect(json_response['id']).to eq(deploy_key.id) - end - end - - context 'when authenticated as non-admin user' do - it 'should return a 404 error' do - post v3_api("/projects/#{project2.id}/#{path}/#{deploy_key.id}/enable", user) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe "DELETE /projects/:id/deploy_keys/:key_id/disable" do - context 'when the user can admin the project' do - it 'disables the key' do - expect do - delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}/disable", admin) - end.to change { project.deploy_keys.count }.from(1).to(0) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['id']).to eq(deploy_key.id) - end - end - - context 'when authenticated as non-admin user' do - it 'should return a 404 error' do - delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}/disable", user) - - expect(response).to have_gitlab_http_status(404) - end - end - end - end -end diff --git a/spec/requests/api/v3/deployments_spec.rb b/spec/requests/api/v3/deployments_spec.rb deleted file mode 100644 index ac86fbea498..00000000000 --- a/spec/requests/api/v3/deployments_spec.rb +++ /dev/null @@ -1,69 +0,0 @@ -require 'spec_helper' - -describe API::V3::Deployments do - let(:user) { create(:user) } - let(:non_member) { create(:user) } - let(:project) { deployment.environment.project } - let!(:deployment) { create(:deployment) } - - before do - project.add_master(user) - end - - shared_examples 'a paginated resources' do - before do - # Fires the request - request - end - - it 'has pagination headers' do - expect(response).to include_pagination_headers - end - end - - describe 'GET /projects/:id/deployments' do - context 'as member of the project' do - it_behaves_like 'a paginated resources' do - let(:request) { get v3_api("/projects/#{project.id}/deployments", user) } - end - - it 'returns projects deployments' do - get v3_api("/projects/#{project.id}/deployments", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to eq(1) - expect(json_response.first['iid']).to eq(deployment.iid) - expect(json_response.first['sha']).to match /\A\h{40}\z/ - end - end - - context 'as non member' do - it 'returns a 404 status code' do - get v3_api("/projects/#{project.id}/deployments", non_member) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe 'GET /projects/:id/deployments/:deployment_id' do - context 'as a member of the project' do - it 'returns the projects deployment' do - get v3_api("/projects/#{project.id}/deployments/#{deployment.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['sha']).to match /\A\h{40}\z/ - expect(json_response['id']).to eq(deployment.id) - end - end - - context 'as non member' do - it 'returns a 404 status code' do - get v3_api("/projects/#{project.id}/deployments/#{deployment.id}", non_member) - - expect(response).to have_gitlab_http_status(404) - end - end - end -end diff --git a/spec/requests/api/v3/environments_spec.rb b/spec/requests/api/v3/environments_spec.rb deleted file mode 100644 index 68be5256b64..00000000000 --- a/spec/requests/api/v3/environments_spec.rb +++ /dev/null @@ -1,163 +0,0 @@ -require 'spec_helper' - -describe API::V3::Environments do - let(:user) { create(:user) } - let(:non_member) { create(:user) } - let(:project) { create(:project, :private, namespace: user.namespace) } - let!(:environment) { create(:environment, project: project) } - - before do - project.add_master(user) - end - - shared_examples 'a paginated resources' do - before do - # Fires the request - request - end - - it 'has pagination headers' do - expect(response.headers).to include('X-Total') - expect(response.headers).to include('X-Total-Pages') - expect(response.headers).to include('X-Per-Page') - expect(response.headers).to include('X-Page') - expect(response.headers).to include('X-Next-Page') - expect(response.headers).to include('X-Prev-Page') - expect(response.headers).to include('Link') - end - end - - describe 'GET /projects/:id/environments' do - context 'as member of the project' do - it_behaves_like 'a paginated resources' do - let(:request) { get v3_api("/projects/#{project.id}/environments", user) } - end - - it 'returns project environments' do - get v3_api("/projects/#{project.id}/environments", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to eq(1) - expect(json_response.first['name']).to eq(environment.name) - expect(json_response.first['external_url']).to eq(environment.external_url) - expect(json_response.first['project']['id']).to eq(project.id) - expect(json_response.first['project']['visibility_level']).to be_present - end - end - - context 'as non member' do - it 'returns a 404 status code' do - get v3_api("/projects/#{project.id}/environments", non_member) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe 'POST /projects/:id/environments' do - context 'as a member' do - it 'creates a environment with valid params' do - post v3_api("/projects/#{project.id}/environments", user), name: "mepmep" - - expect(response).to have_gitlab_http_status(201) - expect(json_response['name']).to eq('mepmep') - expect(json_response['slug']).to eq('mepmep') - expect(json_response['external']).to be nil - end - - it 'requires name to be passed' do - post v3_api("/projects/#{project.id}/environments", user), external_url: 'test.gitlab.com' - - expect(response).to have_gitlab_http_status(400) - end - - it 'returns a 400 if environment already exists' do - post v3_api("/projects/#{project.id}/environments", user), name: environment.name - - expect(response).to have_gitlab_http_status(400) - end - - it 'returns a 400 if slug is specified' do - post v3_api("/projects/#{project.id}/environments", user), name: "foo", slug: "foo" - - expect(response).to have_gitlab_http_status(400) - expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed") - end - end - - context 'a non member' do - it 'rejects the request' do - post v3_api("/projects/#{project.id}/environments", non_member), name: 'gitlab.com' - - expect(response).to have_gitlab_http_status(404) - end - - it 'returns a 400 when the required params are missing' do - post v3_api("/projects/12345/environments", non_member), external_url: 'http://env.git.com' - end - end - end - - describe 'PUT /projects/:id/environments/:environment_id' do - it 'returns a 200 if name and external_url are changed' do - url = 'https://mepmep.whatever.ninja' - put v3_api("/projects/#{project.id}/environments/#{environment.id}", user), - name: 'Mepmep', external_url: url - - expect(response).to have_gitlab_http_status(200) - expect(json_response['name']).to eq('Mepmep') - expect(json_response['external_url']).to eq(url) - end - - it "won't allow slug to be changed" do - slug = environment.slug - api_url = v3_api("/projects/#{project.id}/environments/#{environment.id}", user) - put api_url, slug: slug + "-foo" - - expect(response).to have_gitlab_http_status(400) - expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed") - end - - it "won't update the external_url if only the name is passed" do - url = environment.external_url - put v3_api("/projects/#{project.id}/environments/#{environment.id}", user), - name: 'Mepmep' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['name']).to eq('Mepmep') - expect(json_response['external_url']).to eq(url) - end - - it 'returns a 404 if the environment does not exist' do - put v3_api("/projects/#{project.id}/environments/12345", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'DELETE /projects/:id/environments/:environment_id' do - context 'as a master' do - it 'returns a 200 for an existing environment' do - delete v3_api("/projects/#{project.id}/environments/#{environment.id}", user) - - expect(response).to have_gitlab_http_status(200) - end - - it 'returns a 404 for non existing id' do - delete v3_api("/projects/#{project.id}/environments/12345", user) - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Not found') - end - end - - context 'a non member' do - it 'rejects the request' do - delete v3_api("/projects/#{project.id}/environments/#{environment.id}", non_member) - - expect(response).to have_gitlab_http_status(404) - end - end - end -end diff --git a/spec/requests/api/v3/files_spec.rb b/spec/requests/api/v3/files_spec.rb deleted file mode 100644 index 26a3d8870a0..00000000000 --- a/spec/requests/api/v3/files_spec.rb +++ /dev/null @@ -1,283 +0,0 @@ -require 'spec_helper' - -describe API::V3::Files do - # I have to remove periods from the end of the name - # This happened when the user's name had a suffix (i.e. "Sr.") - # This seems to be what git does under the hood. For example, this commit: - # - # $ git commit --author='Foo Sr. <foo@example.com>' -m 'Where's my trailing period?' - # - # results in this: - # - # $ git show --pretty - # ... - # Author: Foo Sr <foo@example.com> - # ... - - let(:user) { create(:user) } - let!(:project) { create(:project, :repository, namespace: user.namespace ) } - let(:guest) { create(:user) { |u| project.add_guest(u) } } - let(:file_path) { 'files/ruby/popen.rb' } - let(:params) do - { - file_path: file_path, - ref: 'master' - } - end - let(:author_email) { 'user@example.org' } - let(:author_name) { 'John Doe' } - - before { project.add_developer(user) } - - describe "GET /projects/:id/repository/files" do - let(:route) { "/projects/#{project.id}/repository/files" } - - shared_examples_for 'repository files' do - it "returns file info" do - get v3_api(route, current_user), params - - expect(response).to have_gitlab_http_status(200) - expect(json_response['file_path']).to eq(file_path) - expect(json_response['file_name']).to eq('popen.rb') - expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') - expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n") - end - - context 'when no params are given' do - it_behaves_like '400 response' do - let(:request) { get v3_api(route, current_user) } - end - end - - context 'when file_path does not exist' do - let(:params) do - { - file_path: 'app/models/application.rb', - ref: 'master' - } - end - - it_behaves_like '404 response' do - let(:request) { get v3_api(route, current_user), params } - let(:message) { '404 File Not Found' } - end - end - - context 'when repository is disabled' do - include_context 'disabled repository' - - it_behaves_like '403 response' do - let(:request) { get v3_api(route, current_user), params } - end - end - end - - context 'when unauthenticated', 'and project is public' do - it_behaves_like 'repository files' do - let(:project) { create(:project, :public, :repository) } - let(:current_user) { nil } - end - end - - context 'when unauthenticated', 'and project is private' do - it_behaves_like '404 response' do - let(:request) { get v3_api(route), params } - let(:message) { '404 Project Not Found' } - end - end - - context 'when authenticated', 'as a developer' do - it_behaves_like 'repository files' do - let(:current_user) { user } - end - end - - context 'when authenticated', 'as a guest' do - it_behaves_like '403 response' do - let(:request) { get v3_api(route, guest), params } - end - end - end - - describe "POST /projects/:id/repository/files" do - let(:valid_params) do - { - file_path: 'newfile.rb', - branch_name: 'master', - content: 'puts 8', - commit_message: 'Added newfile' - } - end - - it "creates a new file in project repo" do - post v3_api("/projects/#{project.id}/repository/files", user), valid_params - - expect(response).to have_gitlab_http_status(201) - expect(json_response['file_path']).to eq('newfile.rb') - last_commit = project.repository.commit.raw - expect(last_commit.author_email).to eq(user.email) - expect(last_commit.author_name).to eq(user.name) - end - - it "returns a 400 bad request if no params given" do - post v3_api("/projects/#{project.id}/repository/files", user) - - expect(response).to have_gitlab_http_status(400) - end - - it "returns a 400 if editor fails to create file" do - allow_any_instance_of(Repository).to receive(:create_file) - .and_raise(Gitlab::Git::CommitError, 'Cannot create file') - - post v3_api("/projects/#{project.id}/repository/files", user), valid_params - - expect(response).to have_gitlab_http_status(400) - end - - context "when specifying an author" do - it "creates a new file with the specified author" do - valid_params.merge!(author_email: author_email, author_name: author_name) - - post v3_api("/projects/#{project.id}/repository/files", user), valid_params - - expect(response).to have_gitlab_http_status(201) - last_commit = project.repository.commit.raw - expect(last_commit.author_email).to eq(author_email) - expect(last_commit.author_name).to eq(author_name) - end - end - - context 'when the repo is empty' do - let!(:project) { create(:project_empty_repo, namespace: user.namespace ) } - - it "creates a new file in project repo" do - post v3_api("/projects/#{project.id}/repository/files", user), valid_params - - expect(response).to have_gitlab_http_status(201) - expect(json_response['file_path']).to eq('newfile.rb') - last_commit = project.repository.commit.raw - expect(last_commit.author_email).to eq(user.email) - expect(last_commit.author_name).to eq(user.name) - end - end - end - - describe "PUT /projects/:id/repository/files" do - let(:valid_params) do - { - file_path: file_path, - branch_name: 'master', - content: 'puts 8', - commit_message: 'Changed file' - } - end - - it "updates existing file in project repo" do - put v3_api("/projects/#{project.id}/repository/files", user), valid_params - - expect(response).to have_gitlab_http_status(200) - expect(json_response['file_path']).to eq(file_path) - last_commit = project.repository.commit.raw - expect(last_commit.author_email).to eq(user.email) - expect(last_commit.author_name).to eq(user.name) - end - - it "returns a 400 bad request if no params given" do - put v3_api("/projects/#{project.id}/repository/files", user) - - expect(response).to have_gitlab_http_status(400) - end - - context "when specifying an author" do - it "updates a file with the specified author" do - valid_params.merge!(author_email: author_email, author_name: author_name, content: "New content") - - put v3_api("/projects/#{project.id}/repository/files", user), valid_params - - expect(response).to have_gitlab_http_status(200) - last_commit = project.repository.commit.raw - expect(last_commit.author_email).to eq(author_email) - expect(last_commit.author_name).to eq(author_name) - end - end - end - - describe "DELETE /projects/:id/repository/files" do - let(:valid_params) do - { - file_path: file_path, - branch_name: 'master', - commit_message: 'Changed file' - } - end - - it "deletes existing file in project repo" do - delete v3_api("/projects/#{project.id}/repository/files", user), valid_params - - expect(response).to have_gitlab_http_status(200) - expect(json_response['file_path']).to eq(file_path) - last_commit = project.repository.commit.raw - expect(last_commit.author_email).to eq(user.email) - expect(last_commit.author_name).to eq(user.name) - end - - it "returns a 400 bad request if no params given" do - delete v3_api("/projects/#{project.id}/repository/files", user) - - expect(response).to have_gitlab_http_status(400) - end - - it "returns a 400 if fails to delete file" do - allow_any_instance_of(Repository).to receive(:delete_file).and_raise(Gitlab::Git::CommitError, 'Cannot delete file') - - delete v3_api("/projects/#{project.id}/repository/files", user), valid_params - - expect(response).to have_gitlab_http_status(400) - end - - context "when specifying an author" do - it "removes a file with the specified author" do - valid_params.merge!(author_email: author_email, author_name: author_name) - - delete v3_api("/projects/#{project.id}/repository/files", user), valid_params - - expect(response).to have_gitlab_http_status(200) - last_commit = project.repository.commit.raw - expect(last_commit.author_email).to eq(author_email) - expect(last_commit.author_name).to eq(author_name) - end - end - end - - describe "POST /projects/:id/repository/files with binary file" do - let(:file_path) { 'test.bin' } - let(:put_params) do - { - file_path: file_path, - branch_name: 'master', - content: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=', - commit_message: 'Binary file with a \n should not be touched', - encoding: 'base64' - } - end - let(:get_params) do - { - file_path: file_path, - ref: 'master' - } - end - - before do - post v3_api("/projects/#{project.id}/repository/files", user), put_params - end - - it "remains unchanged" do - get v3_api("/projects/#{project.id}/repository/files", user), get_params - - expect(response).to have_gitlab_http_status(200) - expect(json_response['file_path']).to eq(file_path) - expect(json_response['file_name']).to eq(file_path) - expect(json_response['content']).to eq(put_params[:content]) - end - end -end diff --git a/spec/requests/api/v3/groups_spec.rb b/spec/requests/api/v3/groups_spec.rb deleted file mode 100644 index 34d4b8e9565..00000000000 --- a/spec/requests/api/v3/groups_spec.rb +++ /dev/null @@ -1,566 +0,0 @@ -require 'spec_helper' - -describe API::V3::Groups do - include UploadHelpers - - let(:user1) { create(:user, can_create_group: false) } - let(:user2) { create(:user) } - let(:user3) { create(:user) } - let(:admin) { create(:admin) } - let!(:group1) { create(:group, avatar: File.open(uploaded_image_temp_path)) } - let!(:group2) { create(:group, :private) } - let!(:project1) { create(:project, namespace: group1) } - let!(:project2) { create(:project, namespace: group2) } - let!(:project3) { create(:project, namespace: group1, path: 'test', visibility_level: Gitlab::VisibilityLevel::PRIVATE) } - - before do - group1.add_owner(user1) - group2.add_owner(user2) - end - - describe "GET /groups" do - context "when unauthenticated" do - it "returns authentication error" do - get v3_api("/groups") - - expect(response).to have_gitlab_http_status(401) - end - end - - context "when authenticated as user" do - it "normal user: returns an array of groups of user1" do - get v3_api("/groups", user1) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response) - .to satisfy_one { |group| group['name'] == group1.name } - end - - it "does not include statistics" do - get v3_api("/groups", user1), statistics: true - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first).not_to include 'statistics' - end - end - - context "when authenticated as admin" do - it "admin: returns an array of all groups" do - get v3_api("/groups", admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - end - - it "does not include statistics by default" do - get v3_api("/groups", admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first).not_to include('statistics') - end - - it "includes statistics if requested" do - attributes = { - storage_size: 702, - repository_size: 123, - lfs_objects_size: 234, - build_artifacts_size: 345 - }.stringify_keys - - project1.statistics.update!(attributes) - - get v3_api("/groups", admin), statistics: true - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response) - .to satisfy_one { |group| group['statistics'] == attributes } - end - end - - context "when using skip_groups in request" do - it "returns all groups excluding skipped groups" do - get v3_api("/groups", admin), skip_groups: [group2.id] - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - end - end - - context "when using all_available in request" do - let(:response_groups) { json_response.map { |group| group['name'] } } - - it "returns all groups you have access to" do - public_group = create :group, :public - - get v3_api("/groups", user1), all_available: true - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_groups).to contain_exactly(public_group.name, group1.name) - end - end - - context "when using sorting" do - let(:group3) { create(:group, name: "a#{group1.name}", path: "z#{group1.path}") } - let(:response_groups) { json_response.map { |group| group['name'] } } - - before do - group3.add_owner(user1) - end - - it "sorts by name ascending by default" do - get v3_api("/groups", user1) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_groups).to eq([group3.name, group1.name]) - end - - it "sorts in descending order when passed" do - get v3_api("/groups", user1), sort: "desc" - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_groups).to eq([group1.name, group3.name]) - end - - it "sorts by the order_by param" do - get v3_api("/groups", user1), order_by: "path" - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_groups).to eq([group1.name, group3.name]) - end - end - end - - describe 'GET /groups/owned' do - context 'when unauthenticated' do - it 'returns authentication error' do - get v3_api('/groups/owned') - - expect(response).to have_gitlab_http_status(401) - end - end - - context 'when authenticated as group owner' do - it 'returns an array of groups the user owns' do - get v3_api('/groups/owned', user2) - - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.first['name']).to eq(group2.name) - end - end - end - - describe "GET /groups/:id" do - context "when authenticated as user" do - it "returns one of user1's groups" do - project = create(:project, namespace: group2, path: 'Foo') - create(:project_group_link, project: project, group: group1) - - get v3_api("/groups/#{group1.id}", user1) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['id']).to eq(group1.id) - expect(json_response['name']).to eq(group1.name) - expect(json_response['path']).to eq(group1.path) - expect(json_response['description']).to eq(group1.description) - expect(json_response['visibility_level']).to eq(group1.visibility_level) - expect(json_response['avatar_url']).to eq(group1.avatar_url(only_path: false)) - expect(json_response['web_url']).to eq(group1.web_url) - expect(json_response['request_access_enabled']).to eq(group1.request_access_enabled) - expect(json_response['full_name']).to eq(group1.full_name) - expect(json_response['full_path']).to eq(group1.full_path) - expect(json_response['parent_id']).to eq(group1.parent_id) - expect(json_response['projects']).to be_an Array - expect(json_response['projects'].length).to eq(2) - expect(json_response['shared_projects']).to be_an Array - expect(json_response['shared_projects'].length).to eq(1) - expect(json_response['shared_projects'][0]['id']).to eq(project.id) - end - - it "does not return a non existing group" do - get v3_api("/groups/1328", user1) - - expect(response).to have_gitlab_http_status(404) - end - - it "does not return a group not attached to user1" do - get v3_api("/groups/#{group2.id}", user1) - - expect(response).to have_gitlab_http_status(404) - end - end - - context "when authenticated as admin" do - it "returns any existing group" do - get v3_api("/groups/#{group2.id}", admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['name']).to eq(group2.name) - end - - it "does not return a non existing group" do - get v3_api("/groups/1328", admin) - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'when using group path in URL' do - it 'returns any existing group' do - get v3_api("/groups/#{group1.path}", admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['name']).to eq(group1.name) - end - - it 'does not return a non existing group' do - get v3_api('/groups/unknown', admin) - - expect(response).to have_gitlab_http_status(404) - end - - it 'does not return a group not attached to user1' do - get v3_api("/groups/#{group2.path}", user1) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe 'PUT /groups/:id' do - let(:new_group_name) { 'New Group'} - - context 'when authenticated as the group owner' do - it 'updates the group' do - put v3_api("/groups/#{group1.id}", user1), name: new_group_name, request_access_enabled: true - - expect(response).to have_gitlab_http_status(200) - expect(json_response['name']).to eq(new_group_name) - expect(json_response['request_access_enabled']).to eq(true) - end - - it 'returns 404 for a non existing group' do - put v3_api('/groups/1328', user1), name: new_group_name - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'when authenticated as the admin' do - it 'updates the group' do - put v3_api("/groups/#{group1.id}", admin), name: new_group_name - - expect(response).to have_gitlab_http_status(200) - expect(json_response['name']).to eq(new_group_name) - end - end - - context 'when authenticated as an user that can see the group' do - it 'does not updates the group' do - put v3_api("/groups/#{group1.id}", user2), name: new_group_name - - expect(response).to have_gitlab_http_status(403) - end - end - - context 'when authenticated as an user that cannot see the group' do - it 'returns 404 when trying to update the group' do - put v3_api("/groups/#{group2.id}", user1), name: new_group_name - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe "GET /groups/:id/projects" do - context "when authenticated as user" do - it "returns the group's projects" do - get v3_api("/groups/#{group1.id}/projects", user1) - - expect(response).to have_gitlab_http_status(200) - expect(json_response.length).to eq(2) - project_names = json_response.map { |proj| proj['name'] } - expect(project_names).to match_array([project1.name, project3.name]) - expect(json_response.first['visibility_level']).to be_present - end - - it "returns the group's projects with simple representation" do - get v3_api("/groups/#{group1.id}/projects", user1), simple: true - - expect(response).to have_gitlab_http_status(200) - expect(json_response.length).to eq(2) - project_names = json_response.map { |proj| proj['name'] } - expect(project_names).to match_array([project1.name, project3.name]) - expect(json_response.first['visibility_level']).not_to be_present - end - - it 'filters the groups projects' do - public_project = create(:project, :public, path: 'test1', group: group1) - - get v3_api("/groups/#{group1.id}/projects", user1), visibility: 'public' - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an(Array) - expect(json_response.length).to eq(1) - expect(json_response.first['name']).to eq(public_project.name) - end - - it "does not return a non existing group" do - get v3_api("/groups/1328/projects", user1) - - expect(response).to have_gitlab_http_status(404) - end - - it "does not return a group not attached to user1" do - get v3_api("/groups/#{group2.id}/projects", user1) - - expect(response).to have_gitlab_http_status(404) - end - - it "only returns projects to which user has access" do - project3.add_developer(user3) - - get v3_api("/groups/#{group1.id}/projects", user3) - - expect(response).to have_gitlab_http_status(200) - expect(json_response.length).to eq(1) - expect(json_response.first['name']).to eq(project3.name) - end - - it 'only returns the projects owned by user' do - project2.group.add_owner(user3) - - get v3_api("/groups/#{project2.group.id}/projects", user3), owned: true - - expect(response).to have_gitlab_http_status(200) - expect(json_response.length).to eq(1) - expect(json_response.first['name']).to eq(project2.name) - end - - it 'only returns the projects starred by user' do - user1.starred_projects = [project1] - - get v3_api("/groups/#{group1.id}/projects", user1), starred: true - - expect(response).to have_gitlab_http_status(200) - expect(json_response.length).to eq(1) - expect(json_response.first['name']).to eq(project1.name) - end - end - - context "when authenticated as admin" do - it "returns any existing group" do - get v3_api("/groups/#{group2.id}/projects", admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response.length).to eq(1) - expect(json_response.first['name']).to eq(project2.name) - end - - it "does not return a non existing group" do - get v3_api("/groups/1328/projects", admin) - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'when using group path in URL' do - it 'returns any existing group' do - get v3_api("/groups/#{group1.path}/projects", admin) - - expect(response).to have_gitlab_http_status(200) - project_names = json_response.map { |proj| proj['name'] } - expect(project_names).to match_array([project1.name, project3.name]) - end - - it 'does not return a non existing group' do - get v3_api('/groups/unknown/projects', admin) - - expect(response).to have_gitlab_http_status(404) - end - - it 'does not return a group not attached to user1' do - get v3_api("/groups/#{group2.path}/projects", user1) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe "POST /groups" do - context "when authenticated as user without group permissions" do - it "does not create group" do - post v3_api("/groups", user1), attributes_for(:group) - - expect(response).to have_gitlab_http_status(403) - end - end - - context "when authenticated as user with group permissions" do - it "creates group" do - group = attributes_for(:group, { request_access_enabled: false }) - - post v3_api("/groups", user3), group - - expect(response).to have_gitlab_http_status(201) - - expect(json_response["name"]).to eq(group[:name]) - expect(json_response["path"]).to eq(group[:path]) - expect(json_response["request_access_enabled"]).to eq(group[:request_access_enabled]) - end - - it "creates a nested group", :nested_groups do - parent = create(:group) - parent.add_owner(user3) - group = attributes_for(:group, { parent_id: parent.id }) - - post v3_api("/groups", user3), group - - expect(response).to have_gitlab_http_status(201) - - expect(json_response["full_path"]).to eq("#{parent.path}/#{group[:path]}") - expect(json_response["parent_id"]).to eq(parent.id) - end - - it "does not create group, duplicate" do - post v3_api("/groups", user3), { name: 'Duplicate Test', path: group2.path } - - expect(response).to have_gitlab_http_status(400) - expect(response.message).to eq("Bad Request") - end - - it "returns 400 bad request error if name not given" do - post v3_api("/groups", user3), { path: group2.path } - - expect(response).to have_gitlab_http_status(400) - end - - it "returns 400 bad request error if path not given" do - post v3_api("/groups", user3), { name: 'test' } - - expect(response).to have_gitlab_http_status(400) - end - end - end - - describe "DELETE /groups/:id" do - context "when authenticated as user" do - it "removes group" do - Sidekiq::Testing.fake! do - expect { delete v3_api("/groups/#{group1.id}", user1) }.to change(GroupDestroyWorker.jobs, :size).by(1) - end - - expect(response).to have_gitlab_http_status(202) - end - - it "does not remove a group if not an owner" do - user4 = create(:user) - group1.add_master(user4) - - delete v3_api("/groups/#{group1.id}", user3) - - expect(response).to have_gitlab_http_status(403) - end - - it "does not remove a non existing group" do - delete v3_api("/groups/1328", user1) - - expect(response).to have_gitlab_http_status(404) - end - - it "does not remove a group not attached to user1" do - delete v3_api("/groups/#{group2.id}", user1) - - expect(response).to have_gitlab_http_status(404) - end - end - - context "when authenticated as admin" do - it "removes any existing group" do - delete v3_api("/groups/#{group2.id}", admin) - - expect(response).to have_gitlab_http_status(202) - end - - it "does not remove a non existing group" do - delete v3_api("/groups/1328", admin) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe "POST /groups/:id/projects/:project_id" do - let(:project) { create(:project) } - let(:project_path) { CGI.escape(project.full_path) } - - before do - allow_any_instance_of(Projects::TransferService) - .to receive(:execute).and_return(true) - end - - context "when authenticated as user" do - it "does not transfer project to group" do - post v3_api("/groups/#{group1.id}/projects/#{project.id}", user2) - - expect(response).to have_gitlab_http_status(403) - end - end - - context "when authenticated as admin" do - it "transfers project to group" do - post v3_api("/groups/#{group1.id}/projects/#{project.id}", admin) - - expect(response).to have_gitlab_http_status(201) - end - - context 'when using project path in URL' do - context 'with a valid project path' do - it "transfers project to group" do - post v3_api("/groups/#{group1.id}/projects/#{project_path}", admin) - - expect(response).to have_gitlab_http_status(201) - end - end - - context 'with a non-existent project path' do - it "does not transfer project to group" do - post v3_api("/groups/#{group1.id}/projects/nogroup%2Fnoproject", admin) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - context 'when using a group path in URL' do - context 'with a valid group path' do - it "transfers project to group" do - post v3_api("/groups/#{group1.path}/projects/#{project_path}", admin) - - expect(response).to have_gitlab_http_status(201) - end - end - - context 'with a non-existent group path' do - it "does not transfer project to group" do - post v3_api("/groups/noexist/projects/#{project_path}", admin) - - expect(response).to have_gitlab_http_status(404) - end - end - end - end - end -end diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb deleted file mode 100644 index 11b5469be7b..00000000000 --- a/spec/requests/api/v3/issues_spec.rb +++ /dev/null @@ -1,1298 +0,0 @@ -require 'spec_helper' - -describe API::V3::Issues do - set(:user) { create(:user) } - set(:user2) { create(:user) } - set(:non_member) { create(:user) } - set(:guest) { create(:user) } - set(:author) { create(:author) } - set(:assignee) { create(:assignee) } - set(:admin) { create(:user, :admin) } - let!(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) } - let!(:closed_issue) do - create :closed_issue, - author: user, - assignees: [user], - project: project, - state: :closed, - milestone: milestone, - created_at: generate(:past_time), - updated_at: 3.hours.ago - end - let!(:confidential_issue) do - create :issue, - :confidential, - project: project, - author: author, - assignees: [assignee], - created_at: generate(:past_time), - updated_at: 2.hours.ago - end - let!(:issue) do - create :issue, - author: user, - assignees: [user], - project: project, - milestone: milestone, - created_at: generate(:past_time), - updated_at: 1.hour.ago - end - let!(:label) do - create(:label, title: 'label', color: '#FFAABB', project: project) - end - let!(:label_link) { create(:label_link, label: label, target: issue) } - let!(:milestone) { create(:milestone, title: '1.0.0', project: project) } - let!(:empty_milestone) do - create(:milestone, title: '2.0.0', project: project) - end - let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) } - - let(:no_milestone_title) { URI.escape(Milestone::None.title) } - - before do - project.add_reporter(user) - project.add_guest(guest) - end - - describe "GET /issues" do - context "when unauthenticated" do - it "returns authentication error" do - get v3_api("/issues") - - expect(response).to have_gitlab_http_status(401) - end - end - - context "when authenticated" do - it "returns an array of issues" do - get v3_api("/issues", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['title']).to eq(issue.title) - expect(json_response.last).to have_key('web_url') - end - - it 'returns an array of closed issues' do - get v3_api('/issues?state=closed', user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(closed_issue.id) - end - - it 'returns an array of opened issues' do - get v3_api('/issues?state=opened', user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(issue.id) - end - - it 'returns an array of all issues' do - get v3_api('/issues?state=all', user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - expect(json_response.first['id']).to eq(issue.id) - expect(json_response.second['id']).to eq(closed_issue.id) - end - - it 'returns an array of labeled issues' do - get v3_api("/issues?labels=#{label.title}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['labels']).to eq([label.title]) - end - - it 'returns an array of labeled issues when at least one label matches' do - get v3_api("/issues?labels=#{label.title},foo,bar", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['labels']).to eq([label.title]) - end - - it 'returns an empty array if no issue matches labels' do - get v3_api('/issues?labels=foo,bar', user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) - end - - it 'returns an array of labeled issues matching given state' do - get v3_api("/issues?labels=#{label.title}&state=opened", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['labels']).to eq([label.title]) - expect(json_response.first['state']).to eq('opened') - end - - it 'returns an empty array if no issue matches labels and state filters' do - get v3_api("/issues?labels=#{label.title}&state=closed", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) - end - - it 'returns an empty array if no issue matches milestone' do - get v3_api("/issues?milestone=#{empty_milestone.title}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) - end - - it 'returns an empty array if milestone does not exist' do - get v3_api("/issues?milestone=foo", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) - end - - it 'returns an array of issues in given milestone' do - get v3_api("/issues?milestone=#{milestone.title}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - expect(json_response.first['id']).to eq(issue.id) - expect(json_response.second['id']).to eq(closed_issue.id) - end - - it 'returns an array of issues matching state in milestone' do - get v3_api("/issues?milestone=#{milestone.title}", user), - '&state=closed' - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(closed_issue.id) - end - - it 'returns an array of issues with no milestone' do - get v3_api("/issues?milestone=#{no_milestone_title}", author) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(confidential_issue.id) - end - - it 'sorts by created_at descending by default' do - get v3_api('/issues', user) - - response_dates = json_response.map { |issue| issue['created_at'] } - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_dates).to eq(response_dates.sort.reverse) - end - - it 'sorts ascending when requested' do - get v3_api('/issues?sort=asc', user) - - response_dates = json_response.map { |issue| issue['created_at'] } - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_dates).to eq(response_dates.sort) - end - - it 'sorts by updated_at descending when requested' do - get v3_api('/issues?order_by=updated_at', user) - - response_dates = json_response.map { |issue| issue['updated_at'] } - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_dates).to eq(response_dates.sort.reverse) - end - - it 'sorts by updated_at ascending when requested' do - get v3_api('/issues?order_by=updated_at&sort=asc', user) - - response_dates = json_response.map { |issue| issue['updated_at'] } - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_dates).to eq(response_dates.sort) - end - - it 'matches V3 response schema' do - get v3_api('/issues', user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to match_response_schema('public_api/v3/issues') - end - end - end - - describe "GET /groups/:id/issues" do - let!(:group) { create(:group) } - let!(:group_project) { create(:project, :public, creator_id: user.id, namespace: group) } - let!(:group_closed_issue) do - create :closed_issue, - author: user, - assignees: [user], - project: group_project, - state: :closed, - milestone: group_milestone, - updated_at: 3.hours.ago - end - let!(:group_confidential_issue) do - create :issue, - :confidential, - project: group_project, - author: author, - assignees: [assignee], - updated_at: 2.hours.ago - end - let!(:group_issue) do - create :issue, - author: user, - assignees: [user], - project: group_project, - milestone: group_milestone, - updated_at: 1.hour.ago - end - let!(:group_label) do - create(:label, title: 'group_lbl', color: '#FFAABB', project: group_project) - end - let!(:group_label_link) { create(:label_link, label: group_label, target: group_issue) } - let!(:group_milestone) { create(:milestone, title: '3.0.0', project: group_project) } - let!(:group_empty_milestone) do - create(:milestone, title: '4.0.0', project: group_project) - end - let!(:group_note) { create(:note_on_issue, author: user, project: group_project, noteable: group_issue) } - - before do - group_project.add_reporter(user) - end - let(:base_url) { "/groups/#{group.id}/issues" } - - it 'returns all group issues (including opened and closed)' do - get v3_api(base_url, admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) - end - - it 'returns group issues without confidential issues for non project members' do - get v3_api("#{base_url}?state=opened", non_member) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['title']).to eq(group_issue.title) - end - - it 'returns group confidential issues for author' do - get v3_api("#{base_url}?state=opened", author) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - end - - it 'returns group confidential issues for assignee' do - get v3_api("#{base_url}?state=opened", assignee) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - end - - it 'returns group issues with confidential issues for project members' do - get v3_api("#{base_url}?state=opened", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - end - - it 'returns group confidential issues for admin' do - get v3_api("#{base_url}?state=opened", admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - end - - it 'returns an array of labeled group issues' do - get v3_api("#{base_url}?labels=#{group_label.title}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['labels']).to eq([group_label.title]) - end - - it 'returns an array of labeled group issues where all labels match' do - get v3_api("#{base_url}?labels=#{group_label.title},foo,bar", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) - end - - it 'returns an empty array if no group issue matches labels' do - get v3_api("#{base_url}?labels=foo,bar", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) - end - - it 'returns an empty array if no issue matches milestone' do - get v3_api("#{base_url}?milestone=#{group_empty_milestone.title}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) - end - - it 'returns an empty array if milestone does not exist' do - get v3_api("#{base_url}?milestone=foo", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) - end - - it 'returns an array of issues in given milestone' do - get v3_api("#{base_url}?state=opened&milestone=#{group_milestone.title}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(group_issue.id) - end - - it 'returns an array of issues matching state in milestone' do - get v3_api("#{base_url}?milestone=#{group_milestone.title}", user), - '&state=closed' - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(group_closed_issue.id) - end - - it 'returns an array of issues with no milestone' do - get v3_api("#{base_url}?milestone=#{no_milestone_title}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(group_confidential_issue.id) - end - - it 'sorts by created_at descending by default' do - get v3_api(base_url, user) - - response_dates = json_response.map { |issue| issue['created_at'] } - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_dates).to eq(response_dates.sort.reverse) - end - - it 'sorts ascending when requested' do - get v3_api("#{base_url}?sort=asc", user) - - response_dates = json_response.map { |issue| issue['created_at'] } - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_dates).to eq(response_dates.sort) - end - - it 'sorts by updated_at descending when requested' do - get v3_api("#{base_url}?order_by=updated_at", user) - - response_dates = json_response.map { |issue| issue['updated_at'] } - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_dates).to eq(response_dates.sort.reverse) - end - - it 'sorts by updated_at ascending when requested' do - get v3_api("#{base_url}?order_by=updated_at&sort=asc", user) - - response_dates = json_response.map { |issue| issue['updated_at'] } - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_dates).to eq(response_dates.sort) - end - end - - describe "GET /projects/:id/issues" do - let(:base_url) { "/projects/#{project.id}" } - - it 'returns 404 when project does not exist' do - get v3_api('/projects/1000/issues', non_member) - - expect(response).to have_gitlab_http_status(404) - end - - it "returns 404 on private projects for other users" do - private_project = create(:project, :private) - create(:issue, project: private_project) - - get v3_api("/projects/#{private_project.id}/issues", non_member) - - expect(response).to have_gitlab_http_status(404) - end - - it 'returns no issues when user has access to project but not issues' do - restricted_project = create(:project, :public, issues_access_level: ProjectFeature::PRIVATE) - create(:issue, project: restricted_project) - - get v3_api("/projects/#{restricted_project.id}/issues", non_member) - - expect(json_response).to eq([]) - end - - it 'returns project issues without confidential issues for non project members' do - get v3_api("#{base_url}/issues", non_member) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - expect(json_response.first['title']).to eq(issue.title) - end - - it 'returns project issues without confidential issues for project members with guest role' do - get v3_api("#{base_url}/issues", guest) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - expect(json_response.first['title']).to eq(issue.title) - end - - it 'returns project confidential issues for author' do - get v3_api("#{base_url}/issues", author) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) - expect(json_response.first['title']).to eq(issue.title) - end - - it 'returns project confidential issues for assignee' do - get v3_api("#{base_url}/issues", assignee) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) - expect(json_response.first['title']).to eq(issue.title) - end - - it 'returns project issues with confidential issues for project members' do - get v3_api("#{base_url}/issues", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) - expect(json_response.first['title']).to eq(issue.title) - end - - it 'returns project confidential issues for admin' do - get v3_api("#{base_url}/issues", admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) - expect(json_response.first['title']).to eq(issue.title) - end - - it 'returns an array of labeled project issues' do - get v3_api("#{base_url}/issues?labels=#{label.title}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['labels']).to eq([label.title]) - end - - it 'returns an array of labeled project issues where all labels match' do - get v3_api("#{base_url}/issues?labels=#{label.title},foo,bar", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['labels']).to eq([label.title]) - end - - it 'returns an empty array if no project issue matches labels' do - get v3_api("#{base_url}/issues?labels=foo,bar", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) - end - - it 'returns an empty array if no issue matches milestone' do - get v3_api("#{base_url}/issues?milestone=#{empty_milestone.title}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) - end - - it 'returns an empty array if milestone does not exist' do - get v3_api("#{base_url}/issues?milestone=foo", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) - end - - it 'returns an array of issues in given milestone' do - get v3_api("#{base_url}/issues?milestone=#{milestone.title}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - expect(json_response.first['id']).to eq(issue.id) - expect(json_response.second['id']).to eq(closed_issue.id) - end - - it 'returns an array of issues matching state in milestone' do - get v3_api("#{base_url}/issues?milestone=#{milestone.title}", user), - '&state=closed' - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(closed_issue.id) - end - - it 'returns an array of issues with no milestone' do - get v3_api("#{base_url}/issues?milestone=#{no_milestone_title}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(confidential_issue.id) - end - - it 'sorts by created_at descending by default' do - get v3_api("#{base_url}/issues", user) - - response_dates = json_response.map { |issue| issue['created_at'] } - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_dates).to eq(response_dates.sort.reverse) - end - - it 'sorts ascending when requested' do - get v3_api("#{base_url}/issues?sort=asc", user) - - response_dates = json_response.map { |issue| issue['created_at'] } - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_dates).to eq(response_dates.sort) - end - - it 'sorts by updated_at descending when requested' do - get v3_api("#{base_url}/issues?order_by=updated_at", user) - - response_dates = json_response.map { |issue| issue['updated_at'] } - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_dates).to eq(response_dates.sort.reverse) - end - - it 'sorts by updated_at ascending when requested' do - get v3_api("#{base_url}/issues?order_by=updated_at&sort=asc", user) - - response_dates = json_response.map { |issue| issue['updated_at'] } - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_dates).to eq(response_dates.sort) - end - end - - describe "GET /projects/:id/issues/:issue_id" do - it 'exposes known attributes' do - get v3_api("/projects/#{project.id}/issues/#{issue.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['id']).to eq(issue.id) - expect(json_response['iid']).to eq(issue.iid) - expect(json_response['project_id']).to eq(issue.project.id) - expect(json_response['title']).to eq(issue.title) - expect(json_response['description']).to eq(issue.description) - expect(json_response['state']).to eq(issue.state) - expect(json_response['created_at']).to be_present - expect(json_response['updated_at']).to be_present - expect(json_response['labels']).to eq(issue.label_names) - expect(json_response['milestone']).to be_a Hash - expect(json_response['assignee']).to be_a Hash - expect(json_response['author']).to be_a Hash - expect(json_response['confidential']).to be_falsy - end - - it "returns a project issue by id" do - get v3_api("/projects/#{project.id}/issues/#{issue.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq(issue.title) - expect(json_response['iid']).to eq(issue.iid) - end - - it 'returns a project issue by iid' do - get v3_api("/projects/#{project.id}/issues?iid=#{issue.iid}", user) - - expect(response.status).to eq 200 - expect(json_response.length).to eq 1 - expect(json_response.first['title']).to eq issue.title - expect(json_response.first['id']).to eq issue.id - expect(json_response.first['iid']).to eq issue.iid - end - - it 'returns an empty array for an unknown project issue iid' do - get v3_api("/projects/#{project.id}/issues?iid=#{issue.iid + 10}", user) - - expect(response.status).to eq 200 - expect(json_response.length).to eq 0 - end - - it "returns 404 if issue id not found" do - get v3_api("/projects/#{project.id}/issues/54321", user) - - expect(response).to have_gitlab_http_status(404) - end - - context 'confidential issues' do - it "returns 404 for non project members" do - get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member) - - expect(response).to have_gitlab_http_status(404) - end - - it "returns 404 for project members with guest role" do - get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest) - - expect(response).to have_gitlab_http_status(404) - end - - it "returns confidential issue for project members" do - get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq(confidential_issue.title) - expect(json_response['iid']).to eq(confidential_issue.iid) - end - - it "returns confidential issue for author" do - get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", author) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq(confidential_issue.title) - expect(json_response['iid']).to eq(confidential_issue.iid) - end - - it "returns confidential issue for assignee" do - get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", assignee) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq(confidential_issue.title) - expect(json_response['iid']).to eq(confidential_issue.iid) - end - - it "returns confidential issue for admin" do - get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq(confidential_issue.title) - expect(json_response['iid']).to eq(confidential_issue.iid) - end - end - end - - describe "POST /projects/:id/issues" do - it 'creates a new project issue' do - post v3_api("/projects/#{project.id}/issues", user), - title: 'new issue', labels: 'label, label2', assignee_id: assignee.id - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('new issue') - expect(json_response['description']).to be_nil - expect(json_response['labels']).to eq(%w(label label2)) - expect(json_response['confidential']).to be_falsy - expect(json_response['assignee']['name']).to eq(assignee.name) - end - - it 'creates a new confidential project issue' do - post v3_api("/projects/#{project.id}/issues", user), - title: 'new issue', confidential: true - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('new issue') - expect(json_response['confidential']).to be_truthy - end - - it 'creates a new confidential project issue with a different param' do - post v3_api("/projects/#{project.id}/issues", user), - title: 'new issue', confidential: 'y' - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('new issue') - expect(json_response['confidential']).to be_truthy - end - - it 'creates a public issue when confidential param is false' do - post v3_api("/projects/#{project.id}/issues", user), - title: 'new issue', confidential: false - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('new issue') - expect(json_response['confidential']).to be_falsy - end - - it 'creates a public issue when confidential param is invalid' do - post v3_api("/projects/#{project.id}/issues", user), - title: 'new issue', confidential: 'foo' - - expect(response).to have_gitlab_http_status(400) - expect(json_response['error']).to eq('confidential is invalid') - end - - it "returns a 400 bad request if title not given" do - post v3_api("/projects/#{project.id}/issues", user), labels: 'label, label2' - - expect(response).to have_gitlab_http_status(400) - end - - it 'allows special label names' do - post v3_api("/projects/#{project.id}/issues", user), - title: 'new issue', - labels: 'label, label?, label&foo, ?, &' - - expect(response.status).to eq(201) - expect(json_response['labels']).to include 'label' - expect(json_response['labels']).to include 'label?' - expect(json_response['labels']).to include 'label&foo' - expect(json_response['labels']).to include '?' - expect(json_response['labels']).to include '&' - end - - it 'returns 400 if title is too long' do - post v3_api("/projects/#{project.id}/issues", user), - title: 'g' * 256 - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']['title']).to eq([ - 'is too long (maximum is 255 characters)' - ]) - end - - context 'resolving issues in a merge request' do - set(:diff_note_on_merge_request) { create(:diff_note_on_merge_request) } - let(:discussion) { diff_note_on_merge_request.to_discussion } - let(:merge_request) { discussion.noteable } - let(:project) { merge_request.source_project } - before do - project.add_master(user) - post v3_api("/projects/#{project.id}/issues", user), - title: 'New Issue', - merge_request_for_resolving_discussions: merge_request.iid - end - - it 'creates a new project issue' do - expect(response).to have_gitlab_http_status(:created) - end - - it 'resolves the discussions in a merge request' do - discussion.first_note.reload - - expect(discussion.resolved?).to be(true) - end - - it 'assigns a description to the issue mentioning the merge request' do - expect(json_response['description']).to include(merge_request.to_reference) - end - end - - context 'with due date' do - it 'creates a new project issue' do - due_date = 2.weeks.from_now.strftime('%Y-%m-%d') - - post v3_api("/projects/#{project.id}/issues", user), - title: 'new issue', due_date: due_date - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('new issue') - expect(json_response['description']).to be_nil - expect(json_response['due_date']).to eq(due_date) - end - end - - context 'when an admin or owner makes the request' do - it 'accepts the creation date to be set' do - creation_time = 2.weeks.ago - post v3_api("/projects/#{project.id}/issues", user), - title: 'new issue', labels: 'label, label2', created_at: creation_time - - expect(response).to have_gitlab_http_status(201) - expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time) - end - end - - context 'the user can only read the issue' do - it 'cannot create new labels' do - expect do - post v3_api("/projects/#{project.id}/issues", non_member), title: 'new issue', labels: 'label, label2' - end.not_to change { project.labels.count } - end - end - end - - describe 'POST /projects/:id/issues with spam filtering' do - before do - allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true) - allow_any_instance_of(AkismetService).to receive_messages(spam?: true) - end - - let(:params) do - { - title: 'new issue', - description: 'content here', - labels: 'label, label2' - } - end - - it "does not create a new project issue" do - expect { post v3_api("/projects/#{project.id}/issues", user), params }.not_to change(Issue, :count) - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']).to eq({ "error" => "Spam detected" }) - - spam_logs = SpamLog.all - - expect(spam_logs.count).to eq(1) - expect(spam_logs[0].title).to eq('new issue') - expect(spam_logs[0].description).to eq('content here') - expect(spam_logs[0].user).to eq(user) - expect(spam_logs[0].noteable_type).to eq('Issue') - end - end - - describe "PUT /projects/:id/issues/:issue_id to update only title" do - it "updates a project issue" do - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), - title: 'updated title' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq('updated title') - end - - it "returns 404 error if issue id not found" do - put v3_api("/projects/#{project.id}/issues/44444", user), - title: 'updated title' - - expect(response).to have_gitlab_http_status(404) - end - - it 'allows special label names' do - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), - title: 'updated title', - labels: 'label, label?, label&foo, ?, &' - - expect(response.status).to eq(200) - expect(json_response['labels']).to include 'label' - expect(json_response['labels']).to include 'label?' - expect(json_response['labels']).to include 'label&foo' - expect(json_response['labels']).to include '?' - expect(json_response['labels']).to include '&' - end - - context 'confidential issues' do - it "returns 403 for non project members" do - put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member), - title: 'updated title' - - expect(response).to have_gitlab_http_status(403) - end - - it "returns 403 for project members with guest role" do - put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest), - title: 'updated title' - - expect(response).to have_gitlab_http_status(403) - end - - it "updates a confidential issue for project members" do - put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user), - title: 'updated title' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq('updated title') - end - - it "updates a confidential issue for author" do - put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", author), - title: 'updated title' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq('updated title') - end - - it "updates a confidential issue for admin" do - put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin), - title: 'updated title' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq('updated title') - end - - it 'sets an issue to confidential' do - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), - confidential: true - - expect(response).to have_gitlab_http_status(200) - expect(json_response['confidential']).to be_truthy - end - - it 'makes a confidential issue public' do - put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user), - confidential: false - - expect(response).to have_gitlab_http_status(200) - expect(json_response['confidential']).to be_falsy - end - - it 'does not update a confidential issue with wrong confidential flag' do - put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user), - confidential: 'foo' - - expect(response).to have_gitlab_http_status(400) - expect(json_response['error']).to eq('confidential is invalid') - end - end - end - - describe 'PUT /projects/:id/issues/:issue_id with spam filtering' do - let(:params) do - { - title: 'updated title', - description: 'content here', - labels: 'label, label2' - } - end - - it "does not create a new project issue" do - allow_any_instance_of(SpamService).to receive_messages(check_for_spam?: true) - allow_any_instance_of(AkismetService).to receive_messages(spam?: true) - - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), params - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']).to eq({ "error" => "Spam detected" }) - - spam_logs = SpamLog.all - expect(spam_logs.count).to eq(1) - expect(spam_logs[0].title).to eq('updated title') - expect(spam_logs[0].description).to eq('content here') - expect(spam_logs[0].user).to eq(user) - expect(spam_logs[0].noteable_type).to eq('Issue') - end - end - - describe 'PUT /projects/:id/issues/:issue_id to update labels' do - let!(:label) { create(:label, title: 'dummy', project: project) } - let!(:label_link) { create(:label_link, label: label, target: issue) } - - it 'does not update labels if not present' do - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), - title: 'updated title' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['labels']).to eq([label.title]) - end - - it 'removes all labels' do - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), labels: '' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['labels']).to eq([]) - end - - it 'updates labels' do - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), - labels: 'foo,bar' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['labels']).to include 'foo' - expect(json_response['labels']).to include 'bar' - end - - it 'allows special label names' do - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), - labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&' - - expect(response.status).to eq(200) - expect(json_response['labels']).to include 'label:foo' - expect(json_response['labels']).to include 'label-bar' - expect(json_response['labels']).to include 'label_bar' - expect(json_response['labels']).to include 'label/bar' - expect(json_response['labels']).to include 'label?bar' - expect(json_response['labels']).to include 'label&bar' - expect(json_response['labels']).to include '?' - expect(json_response['labels']).to include '&' - end - - it 'returns 400 if title is too long' do - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), - title: 'g' * 256 - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']['title']).to eq([ - 'is too long (maximum is 255 characters)' - ]) - end - end - - describe "PUT /projects/:id/issues/:issue_id to update state and label" do - it "updates a project issue" do - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), - labels: 'label2', state_event: "close" - - expect(response).to have_gitlab_http_status(200) - expect(json_response['labels']).to include 'label2' - expect(json_response['state']).to eq "closed" - end - - it 'reopens a project isssue' do - put v3_api("/projects/#{project.id}/issues/#{closed_issue.id}", user), state_event: 'reopen' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['state']).to eq 'opened' - end - - context 'when an admin or owner makes the request' do - it 'accepts the update date to be set' do - update_time = 2.weeks.ago - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), - labels: 'label3', state_event: 'close', updated_at: update_time - - expect(response).to have_gitlab_http_status(200) - expect(json_response['labels']).to include 'label3' - expect(Time.parse(json_response['updated_at'])).to be_like_time(update_time) - end - end - end - - describe 'PUT /projects/:id/issues/:issue_id to update due date' do - it 'creates a new project issue' do - due_date = 2.weeks.from_now.strftime('%Y-%m-%d') - - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), due_date: due_date - - expect(response).to have_gitlab_http_status(200) - expect(json_response['due_date']).to eq(due_date) - end - end - - describe 'PUT /projects/:id/issues/:issue_id to update assignee' do - it 'updates an issue with no assignee' do - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), assignee_id: 0 - - expect(response).to have_gitlab_http_status(200) - expect(json_response['assignee']).to eq(nil) - end - - it 'updates an issue with assignee' do - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), assignee_id: user2.id - - expect(response).to have_gitlab_http_status(200) - expect(json_response['assignee']['name']).to eq(user2.name) - end - end - - describe "DELETE /projects/:id/issues/:issue_id" do - it "rejects a non member from deleting an issue" do - delete v3_api("/projects/#{project.id}/issues/#{issue.id}", non_member) - - expect(response).to have_gitlab_http_status(403) - end - - it "rejects a developer from deleting an issue" do - delete v3_api("/projects/#{project.id}/issues/#{issue.id}", author) - - expect(response).to have_gitlab_http_status(403) - end - - context "when the user is project owner" do - set(:owner) { create(:user) } - let(:project) { create(:project, namespace: owner.namespace) } - - it "deletes the issue if an admin requests it" do - delete v3_api("/projects/#{project.id}/issues/#{issue.id}", owner) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['state']).to eq 'opened' - end - end - - context 'when issue does not exist' do - it 'returns 404 when trying to move an issue' do - delete v3_api("/projects/#{project.id}/issues/123", user) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe '/projects/:id/issues/:issue_id/move' do - let!(:target_project) { create(:project, creator_id: user.id, namespace: user.namespace ) } - let!(:target_project2) { create(:project, creator_id: non_member.id, namespace: non_member.namespace ) } - - it 'moves an issue' do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user), - to_project_id: target_project.id - - expect(response).to have_gitlab_http_status(201) - expect(json_response['project_id']).to eq(target_project.id) - end - - context 'when source and target projects are the same' do - it 'returns 400 when trying to move an issue' do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user), - to_project_id: project.id - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']).to eq('Cannot move issue to project it originates from!') - end - end - - context 'when the user does not have the permission to move issues' do - it 'returns 400 when trying to move an issue' do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user), - to_project_id: target_project2.id - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']).to eq('Cannot move issue due to insufficient permissions!') - end - end - - it 'moves the issue to another namespace if I am admin' do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", admin), - to_project_id: target_project2.id - - expect(response).to have_gitlab_http_status(201) - expect(json_response['project_id']).to eq(target_project2.id) - end - - context 'when issue does not exist' do - it 'returns 404 when trying to move an issue' do - post v3_api("/projects/#{project.id}/issues/123/move", user), - to_project_id: target_project.id - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Issue Not Found') - end - end - - context 'when source project does not exist' do - it 'returns 404 when trying to move an issue' do - post v3_api("/projects/0/issues/#{issue.id}/move", user), - to_project_id: target_project.id - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Project Not Found') - end - end - - context 'when target project does not exist' do - it 'returns 404 when trying to move an issue' do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user), - to_project_id: 0 - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe 'POST :id/issues/:issue_id/subscription' do - it 'subscribes to an issue' do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2) - - expect(response).to have_gitlab_http_status(201) - expect(json_response['subscribed']).to eq(true) - end - - it 'returns 304 if already subscribed' do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user) - - expect(response).to have_gitlab_http_status(304) - end - - it 'returns 404 if the issue is not found' do - post v3_api("/projects/#{project.id}/issues/123/subscription", user) - - expect(response).to have_gitlab_http_status(404) - end - - it 'returns 404 if the issue is confidential' do - post v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'DELETE :id/issues/:issue_id/subscription' do - it 'unsubscribes from an issue' do - delete v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['subscribed']).to eq(false) - end - - it 'returns 304 if not subscribed' do - delete v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2) - - expect(response).to have_gitlab_http_status(304) - end - - it 'returns 404 if the issue is not found' do - delete v3_api("/projects/#{project.id}/issues/123/subscription", user) - - expect(response).to have_gitlab_http_status(404) - end - - it 'returns 404 if the issue is confidential' do - delete v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'time tracking endpoints' do - let(:issuable) { issue } - - include_examples 'V3 time tracking endpoints', 'issue' - end -end diff --git a/spec/requests/api/v3/labels_spec.rb b/spec/requests/api/v3/labels_spec.rb deleted file mode 100644 index cdab4d2bd73..00000000000 --- a/spec/requests/api/v3/labels_spec.rb +++ /dev/null @@ -1,169 +0,0 @@ -require 'spec_helper' - -describe API::V3::Labels do - let(:user) { create(:user) } - let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } - let!(:label1) { create(:label, title: 'label1', project: project) } - let!(:priority_label) { create(:label, title: 'bug', project: project, priority: 3) } - - before do - project.add_master(user) - end - - describe 'GET /projects/:id/labels' do - it 'returns all available labels to the project' do - group = create(:group) - group_label = create(:group_label, title: 'feature', group: group) - project.update(group: group) - create(:labeled_issue, project: project, labels: [group_label], author: user) - create(:labeled_issue, project: project, labels: [label1], author: user, state: :closed) - create(:labeled_merge_request, labels: [priority_label], author: user, source_project: project ) - - expected_keys = %w( - id name color description - open_issues_count closed_issues_count open_merge_requests_count - subscribed priority - ) - - get v3_api("/projects/#{project.id}/labels", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to eq(3) - expect(json_response.first.keys).to match_array expected_keys - expect(json_response.map { |l| l['name'] }).to match_array([group_label.name, priority_label.name, label1.name]) - - label1_response = json_response.find { |l| l['name'] == label1.title } - group_label_response = json_response.find { |l| l['name'] == group_label.title } - priority_label_response = json_response.find { |l| l['name'] == priority_label.title } - - expect(label1_response['open_issues_count']).to eq(0) - expect(label1_response['closed_issues_count']).to eq(1) - expect(label1_response['open_merge_requests_count']).to eq(0) - expect(label1_response['name']).to eq(label1.name) - expect(label1_response['color']).to be_present - expect(label1_response['description']).to be_nil - expect(label1_response['priority']).to be_nil - expect(label1_response['subscribed']).to be_falsey - - expect(group_label_response['open_issues_count']).to eq(1) - expect(group_label_response['closed_issues_count']).to eq(0) - expect(group_label_response['open_merge_requests_count']).to eq(0) - expect(group_label_response['name']).to eq(group_label.name) - expect(group_label_response['color']).to be_present - expect(group_label_response['description']).to be_nil - expect(group_label_response['priority']).to be_nil - expect(group_label_response['subscribed']).to be_falsey - - expect(priority_label_response['open_issues_count']).to eq(0) - expect(priority_label_response['closed_issues_count']).to eq(0) - expect(priority_label_response['open_merge_requests_count']).to eq(1) - expect(priority_label_response['name']).to eq(priority_label.name) - expect(priority_label_response['color']).to be_present - expect(priority_label_response['description']).to be_nil - expect(priority_label_response['priority']).to eq(3) - expect(priority_label_response['subscribed']).to be_falsey - end - end - - describe "POST /projects/:id/labels/:label_id/subscription" do - context "when label_id is a label title" do - it "subscribes to the label" do - post v3_api("/projects/#{project.id}/labels/#{label1.title}/subscription", user) - - expect(response).to have_gitlab_http_status(201) - expect(json_response["name"]).to eq(label1.title) - expect(json_response["subscribed"]).to be_truthy - end - end - - context "when label_id is a label ID" do - it "subscribes to the label" do - post v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) - - expect(response).to have_gitlab_http_status(201) - expect(json_response["name"]).to eq(label1.title) - expect(json_response["subscribed"]).to be_truthy - end - end - - context "when user is already subscribed to label" do - before { label1.subscribe(user, project) } - - it "returns 304" do - post v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) - - expect(response).to have_gitlab_http_status(304) - end - end - - context "when label ID is not found" do - it "returns 404 error" do - post v3_api("/projects/#{project.id}/labels/1234/subscription", user) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe "DELETE /projects/:id/labels/:label_id/subscription" do - before { label1.subscribe(user, project) } - - context "when label_id is a label title" do - it "unsubscribes from the label" do - delete v3_api("/projects/#{project.id}/labels/#{label1.title}/subscription", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response["name"]).to eq(label1.title) - expect(json_response["subscribed"]).to be_falsey - end - end - - context "when label_id is a label ID" do - it "unsubscribes from the label" do - delete v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response["name"]).to eq(label1.title) - expect(json_response["subscribed"]).to be_falsey - end - end - - context "when user is already unsubscribed from label" do - before { label1.unsubscribe(user, project) } - - it "returns 304" do - delete v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) - - expect(response).to have_gitlab_http_status(304) - end - end - - context "when label ID is not found" do - it "returns 404 error" do - delete v3_api("/projects/#{project.id}/labels/1234/subscription", user) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe 'DELETE /projects/:id/labels' do - it 'returns 200 for existing label' do - delete v3_api("/projects/#{project.id}/labels", user), name: 'label1' - - expect(response).to have_gitlab_http_status(200) - end - - it 'returns 404 for non existing label' do - delete v3_api("/projects/#{project.id}/labels", user), name: 'label2' - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Label Not Found') - end - - it 'returns 400 for wrong parameters' do - delete v3_api("/projects/#{project.id}/labels", user) - expect(response).to have_gitlab_http_status(400) - end - end -end diff --git a/spec/requests/api/v3/members_spec.rb b/spec/requests/api/v3/members_spec.rb deleted file mode 100644 index de4339ecb8b..00000000000 --- a/spec/requests/api/v3/members_spec.rb +++ /dev/null @@ -1,350 +0,0 @@ -require 'spec_helper' - -describe API::V3::Members do - let(:master) { create(:user, username: 'master_user') } - let(:developer) { create(:user) } - let(:access_requester) { create(:user) } - let(:stranger) { create(:user) } - - let(:project) do - create(:project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project| - project.add_developer(developer) - project.add_master(master) - project.request_access(access_requester) - end - end - - let!(:group) do - create(:group, :public, :access_requestable) do |group| - group.add_developer(developer) - group.add_owner(master) - group.request_access(access_requester) - end - end - - shared_examples 'GET /:sources/:id/members' do |source_type| - context "with :sources == #{source_type.pluralize}" do - it_behaves_like 'a 404 response when source is private' do - let(:route) { get v3_api("/#{source_type.pluralize}/#{source.id}/members", stranger) } - end - - %i[master developer access_requester stranger].each do |type| - context "when authenticated as a #{type}" do - it 'returns 200' do - user = public_send(type) - get v3_api("/#{source_type.pluralize}/#{source.id}/members", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response.size).to eq(2) - expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id] - end - end - end - - it 'does not return invitees' do - create(:"#{source_type}_member", invite_token: '123', invite_email: 'test@abc.com', source: source, user: nil) - - get v3_api("/#{source_type.pluralize}/#{source.id}/members", developer) - - expect(response).to have_gitlab_http_status(200) - expect(json_response.size).to eq(2) - expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id] - end - - it 'finds members with query string' do - get v3_api("/#{source_type.pluralize}/#{source.id}/members", developer), query: master.username - - expect(response).to have_gitlab_http_status(200) - expect(json_response.count).to eq(1) - expect(json_response.first['username']).to eq(master.username) - end - - it 'finds all members with no query specified' do - get v3_api("/#{source_type.pluralize}/#{source.id}/members", developer), query: '' - - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.count).to eq(2) - expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id] - end - end - end - - shared_examples 'GET /:sources/:id/members/:user_id' do |source_type| - context "with :sources == #{source_type.pluralize}" do - it_behaves_like 'a 404 response when source is private' do - let(:route) { get v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) } - end - - context 'when authenticated as a non-member' do - %i[access_requester stranger].each do |type| - context "as a #{type}" do - it 'returns 200' do - user = public_send(type) - get v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user) - - expect(response).to have_gitlab_http_status(200) - # User attributes - expect(json_response['id']).to eq(developer.id) - expect(json_response['name']).to eq(developer.name) - expect(json_response['username']).to eq(developer.username) - expect(json_response['state']).to eq(developer.state) - expect(json_response['avatar_url']).to eq(developer.avatar_url) - expect(json_response['web_url']).to eq(Gitlab::Routing.url_helpers.user_url(developer)) - - # Member attributes - expect(json_response['access_level']).to eq(Member::DEVELOPER) - end - end - end - end - end - end - - shared_examples 'POST /:sources/:id/members' do |source_type| - context "with :sources == #{source_type.pluralize}" do - it_behaves_like 'a 404 response when source is private' do - let(:route) do - post v3_api("/#{source_type.pluralize}/#{source.id}/members", stranger), - user_id: access_requester.id, access_level: Member::MASTER - end - end - - context 'when authenticated as a non-member or member with insufficient rights' do - %i[access_requester stranger developer].each do |type| - context "as a #{type}" do - it 'returns 403' do - user = public_send(type) - post v3_api("/#{source_type.pluralize}/#{source.id}/members", user), - user_id: access_requester.id, access_level: Member::MASTER - - expect(response).to have_gitlab_http_status(403) - end - end - end - end - - context 'when authenticated as a master/owner' do - context 'and new member is already a requester' do - it 'transforms the requester into a proper member' do - expect do - post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), - user_id: access_requester.id, access_level: Member::MASTER - - expect(response).to have_gitlab_http_status(201) - end.to change { source.members.count }.by(1) - expect(source.requesters.count).to eq(0) - expect(json_response['id']).to eq(access_requester.id) - expect(json_response['access_level']).to eq(Member::MASTER) - end - end - - it 'creates a new member' do - expect do - post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), - user_id: stranger.id, access_level: Member::DEVELOPER, expires_at: '2016-08-05' - - expect(response).to have_gitlab_http_status(201) - end.to change { source.members.count }.by(1) - expect(json_response['id']).to eq(stranger.id) - expect(json_response['access_level']).to eq(Member::DEVELOPER) - expect(json_response['expires_at']).to eq('2016-08-05') - end - end - - it "returns #{source_type == 'project' ? 201 : 409} if member already exists" do - post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), - user_id: master.id, access_level: Member::MASTER - - expect(response).to have_gitlab_http_status(source_type == 'project' ? 201 : 409) - end - - it 'returns 400 when user_id is not given' do - post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), - access_level: Member::MASTER - - expect(response).to have_gitlab_http_status(400) - end - - it 'returns 400 when access_level is not given' do - post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), - user_id: stranger.id - - expect(response).to have_gitlab_http_status(400) - end - - it 'returns 422 when access_level is not valid' do - post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), - user_id: stranger.id, access_level: 1234 - - expect(response).to have_gitlab_http_status(422) - end - end - end - - shared_examples 'PUT /:sources/:id/members/:user_id' do |source_type| - context "with :sources == #{source_type.pluralize}" do - it_behaves_like 'a 404 response when source is private' do - let(:route) do - put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger), - access_level: Member::MASTER - end - end - - context 'when authenticated as a non-member or member with insufficient rights' do - %i[access_requester stranger developer].each do |type| - context "as a #{type}" do - it 'returns 403' do - user = public_send(type) - put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user), - access_level: Member::MASTER - - expect(response).to have_gitlab_http_status(403) - end - end - end - end - - context 'when authenticated as a master/owner' do - it 'updates the member' do - put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master), - access_level: Member::MASTER, expires_at: '2016-08-05' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['id']).to eq(developer.id) - expect(json_response['access_level']).to eq(Member::MASTER) - expect(json_response['expires_at']).to eq('2016-08-05') - end - end - - it 'returns 409 if member does not exist' do - put v3_api("/#{source_type.pluralize}/#{source.id}/members/123", master), - access_level: Member::MASTER - - expect(response).to have_gitlab_http_status(404) - end - - it 'returns 400 when access_level is not given' do - put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master) - - expect(response).to have_gitlab_http_status(400) - end - - it 'returns 422 when access level is not valid' do - put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master), - access_level: 1234 - - expect(response).to have_gitlab_http_status(422) - end - end - end - - shared_examples 'DELETE /:sources/:id/members/:user_id' do |source_type| - context "with :sources == #{source_type.pluralize}" do - it_behaves_like 'a 404 response when source is private' do - let(:route) { delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) } - end - - context 'when authenticated as a non-member or member with insufficient rights' do - %i[access_requester stranger].each do |type| - context "as a #{type}" do - it 'returns 403' do - user = public_send(type) - delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user) - - expect(response).to have_gitlab_http_status(403) - end - end - end - end - - context 'when authenticated as a member and deleting themself' do - it 'deletes the member' do - expect do - delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", developer) - - expect(response).to have_gitlab_http_status(200) - end.to change { source.members.count }.by(-1) - end - end - - context 'when authenticated as a master/owner' do - context 'and member is a requester' do - it "returns #{source_type == 'project' ? 200 : 404}" do - expect do - delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{access_requester.id}", master) - - expect(response).to have_gitlab_http_status(source_type == 'project' ? 200 : 404) - end.not_to change { source.requesters.count } - end - end - - it 'deletes the member' do - expect do - delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master) - - expect(response).to have_gitlab_http_status(200) - end.to change { source.members.count }.by(-1) - end - end - - it "returns #{source_type == 'project' ? 200 : 404} if member does not exist" do - delete v3_api("/#{source_type.pluralize}/#{source.id}/members/123", master) - - expect(response).to have_gitlab_http_status(source_type == 'project' ? 200 : 404) - end - end - end - - it_behaves_like 'GET /:sources/:id/members', 'project' do - let(:source) { project } - end - - it_behaves_like 'GET /:sources/:id/members', 'group' do - let(:source) { group } - end - - it_behaves_like 'GET /:sources/:id/members/:user_id', 'project' do - let(:source) { project } - end - - it_behaves_like 'GET /:sources/:id/members/:user_id', 'group' do - let(:source) { group } - end - - it_behaves_like 'POST /:sources/:id/members', 'project' do - let(:source) { project } - end - - it_behaves_like 'POST /:sources/:id/members', 'group' do - let(:source) { group } - end - - it_behaves_like 'PUT /:sources/:id/members/:user_id', 'project' do - let(:source) { project } - end - - it_behaves_like 'PUT /:sources/:id/members/:user_id', 'group' do - let(:source) { group } - end - - it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'project' do - let(:source) { project } - end - - it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'group' do - let(:source) { group } - end - - context 'Adding owner to project' do - it 'returns 403' do - expect do - post v3_api("/projects/#{project.id}/members", master), - user_id: stranger.id, access_level: Member::OWNER - - expect(response).to have_gitlab_http_status(422) - end.to change { project.members.count }.by(0) - end - end -end diff --git a/spec/requests/api/v3/merge_request_diffs_spec.rb b/spec/requests/api/v3/merge_request_diffs_spec.rb deleted file mode 100644 index 547c066fadc..00000000000 --- a/spec/requests/api/v3/merge_request_diffs_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -require "spec_helper" - -describe API::V3::MergeRequestDiffs, 'MergeRequestDiffs' do - let!(:user) { create(:user) } - let!(:merge_request) { create(:merge_request, importing: true) } - let!(:project) { merge_request.target_project } - - before do - merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') - merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') - project.add_master(user) - end - - describe 'GET /projects/:id/merge_requests/:merge_request_id/versions' do - it 'returns 200 for a valid merge request' do - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user) - merge_request_diff = merge_request.merge_request_diffs.last - - expect(response.status).to eq 200 - expect(json_response.size).to eq(merge_request.merge_request_diffs.size) - expect(json_response.first['id']).to eq(merge_request_diff.id) - expect(json_response.first['head_commit_sha']).to eq(merge_request_diff.head_commit_sha) - end - - it 'returns a 404 when merge_request_id not found' do - get v3_api("/projects/#{project.id}/merge_requests/999/versions", user) - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id' do - it 'returns a 200 for a valid merge request' do - merge_request_diff = merge_request.merge_request_diffs.first - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user) - - expect(response.status).to eq 200 - expect(json_response['id']).to eq(merge_request_diff.id) - expect(json_response['head_commit_sha']).to eq(merge_request_diff.head_commit_sha) - expect(json_response['diffs'].size).to eq(merge_request_diff.diffs.size) - end - - it 'returns a 404 when merge_request_id not found' do - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/999", user) - - expect(response).to have_gitlab_http_status(404) - end - end -end diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb deleted file mode 100644 index be70cb24dce..00000000000 --- a/spec/requests/api/v3/merge_requests_spec.rb +++ /dev/null @@ -1,749 +0,0 @@ -require "spec_helper" - -describe API::MergeRequests do - include ProjectForksHelper - - let(:base_time) { Time.now } - let(:user) { create(:user) } - let(:admin) { create(:user, :admin) } - let(:non_member) { create(:user) } - let!(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace) } - let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, title: "Test", created_at: base_time) } - let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, title: "Closed test", created_at: base_time + 1.second) } - let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') } - let(:milestone) { create(:milestone, title: '1.0.0', project: project) } - - before do - project.add_reporter(user) - end - - describe "GET /projects/:id/merge_requests" do - context "when unauthenticated" do - it "returns authentication error" do - get v3_api("/projects/#{project.id}/merge_requests") - expect(response).to have_gitlab_http_status(401) - end - end - - context "when authenticated" do - it "returns an array of all merge_requests" do - get v3_api("/projects/#{project.id}/merge_requests", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) - expect(json_response.last['title']).to eq(merge_request.title) - expect(json_response.last).to have_key('web_url') - expect(json_response.last['sha']).to eq(merge_request.diff_head_sha) - expect(json_response.last['merge_commit_sha']).to be_nil - expect(json_response.last['merge_commit_sha']).to eq(merge_request.merge_commit_sha) - expect(json_response.first['title']).to eq(merge_request_merged.title) - expect(json_response.first['sha']).to eq(merge_request_merged.diff_head_sha) - expect(json_response.first['merge_commit_sha']).not_to be_nil - expect(json_response.first['merge_commit_sha']).to eq(merge_request_merged.merge_commit_sha) - end - - it "returns an array of all merge_requests" do - get v3_api("/projects/#{project.id}/merge_requests?state", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) - expect(json_response.last['title']).to eq(merge_request.title) - end - - it "returns an array of open merge_requests" do - get v3_api("/projects/#{project.id}/merge_requests?state=opened", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.last['title']).to eq(merge_request.title) - end - - it "returns an array of closed merge_requests" do - get v3_api("/projects/#{project.id}/merge_requests?state=closed", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['title']).to eq(merge_request_closed.title) - end - - it "returns an array of merged merge_requests" do - get v3_api("/projects/#{project.id}/merge_requests?state=merged", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['title']).to eq(merge_request_merged.title) - end - - it 'matches V3 response schema' do - get v3_api("/projects/#{project.id}/merge_requests", user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to match_response_schema('public_api/v3/merge_requests') - end - - context "with ordering" do - before do - @mr_later = mr_with_later_created_and_updated_at_time - @mr_earlier = mr_with_earlier_created_and_updated_at_time - end - - it "returns an array of merge_requests in ascending order" do - get v3_api("/projects/#{project.id}/merge_requests?sort=asc", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) - response_dates = json_response.map { |merge_request| merge_request['created_at'] } - expect(response_dates).to eq(response_dates.sort) - end - - it "returns an array of merge_requests in descending order" do - get v3_api("/projects/#{project.id}/merge_requests?sort=desc", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) - response_dates = json_response.map { |merge_request| merge_request['created_at'] } - expect(response_dates).to eq(response_dates.sort.reverse) - end - - it "returns an array of merge_requests ordered by updated_at" do - get v3_api("/projects/#{project.id}/merge_requests?order_by=updated_at", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) - response_dates = json_response.map { |merge_request| merge_request['updated_at'] } - expect(response_dates).to eq(response_dates.sort.reverse) - end - - it "returns an array of merge_requests ordered by created_at" do - get v3_api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) - response_dates = json_response.map { |merge_request| merge_request['created_at'] } - expect(response_dates).to eq(response_dates.sort) - end - end - end - end - - describe "GET /projects/:id/merge_requests/:merge_request_id" do - it 'exposes known attributes' do - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['id']).to eq(merge_request.id) - expect(json_response['iid']).to eq(merge_request.iid) - expect(json_response['project_id']).to eq(merge_request.project.id) - expect(json_response['title']).to eq(merge_request.title) - expect(json_response['description']).to eq(merge_request.description) - expect(json_response['state']).to eq(merge_request.state) - expect(json_response['created_at']).to be_present - expect(json_response['updated_at']).to be_present - expect(json_response['labels']).to eq(merge_request.label_names) - expect(json_response['milestone']).to be_nil - expect(json_response['assignee']).to be_a Hash - expect(json_response['author']).to be_a Hash - expect(json_response['target_branch']).to eq(merge_request.target_branch) - expect(json_response['source_branch']).to eq(merge_request.source_branch) - expect(json_response['upvotes']).to eq(0) - expect(json_response['downvotes']).to eq(0) - expect(json_response['source_project_id']).to eq(merge_request.source_project.id) - expect(json_response['target_project_id']).to eq(merge_request.target_project.id) - expect(json_response['work_in_progress']).to be_falsy - expect(json_response['merge_when_build_succeeds']).to be_falsy - expect(json_response['merge_status']).to eq('can_be_merged') - expect(json_response['should_close_merge_request']).to be_falsy - expect(json_response['force_close_merge_request']).to be_falsy - end - - it "returns merge_request" do - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq(merge_request.title) - expect(json_response['iid']).to eq(merge_request.iid) - expect(json_response['work_in_progress']).to eq(false) - expect(json_response['merge_status']).to eq('can_be_merged') - expect(json_response['should_close_merge_request']).to be_falsy - expect(json_response['force_close_merge_request']).to be_falsy - end - - it 'returns merge_request by iid' do - url = "/projects/#{project.id}/merge_requests?iid=#{merge_request.iid}" - get v3_api(url, user) - expect(response.status).to eq 200 - expect(json_response.first['title']).to eq merge_request.title - expect(json_response.first['id']).to eq merge_request.id - end - - it 'returns merge_request by iid array' do - get v3_api("/projects/#{project.id}/merge_requests", user), iid: [merge_request.iid, merge_request_closed.iid] - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - expect(json_response.first['title']).to eq merge_request_closed.title - expect(json_response.first['id']).to eq merge_request_closed.id - end - - it "returns a 404 error if merge_request_id not found" do - get v3_api("/projects/#{project.id}/merge_requests/999", user) - expect(response).to have_gitlab_http_status(404) - end - - context 'Work in Progress' do - let!(:merge_request_wip) { create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) } - - it "returns merge_request" do - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request_wip.id}", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response['work_in_progress']).to eq(true) - end - end - end - - describe 'GET /projects/:id/merge_requests/:merge_request_id/commits' do - it 'returns a 200 when merge request is valid' do - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user) - commit = merge_request.commits.first - - expect(response.status).to eq 200 - expect(json_response.size).to eq(merge_request.commits.size) - expect(json_response.first['id']).to eq(commit.id) - expect(json_response.first['title']).to eq(commit.title) - end - - it 'returns a 404 when merge_request_id not found' do - get v3_api("/projects/#{project.id}/merge_requests/999/commits", user) - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'GET /projects/:id/merge_requests/:merge_request_id/changes' do - it 'returns the change information of the merge_request' do - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user) - expect(response.status).to eq 200 - expect(json_response['changes'].size).to eq(merge_request.diffs.size) - end - - it 'returns a 404 when merge_request_id not found' do - get v3_api("/projects/#{project.id}/merge_requests/999/changes", user) - expect(response).to have_gitlab_http_status(404) - end - end - - describe "POST /projects/:id/merge_requests" do - context 'between branches projects' do - it "returns merge_request" do - post v3_api("/projects/#{project.id}/merge_requests", user), - title: 'Test merge_request', - source_branch: 'feature_conflict', - target_branch: 'master', - author: user, - labels: 'label, label2', - milestone_id: milestone.id, - remove_source_branch: true - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('Test merge_request') - expect(json_response['labels']).to eq(%w(label label2)) - expect(json_response['milestone']['id']).to eq(milestone.id) - expect(json_response['force_remove_source_branch']).to be_truthy - end - - it "returns 422 when source_branch equals target_branch" do - post v3_api("/projects/#{project.id}/merge_requests", user), - title: "Test merge_request", source_branch: "master", target_branch: "master", author: user - expect(response).to have_gitlab_http_status(422) - end - - it "returns 400 when source_branch is missing" do - post v3_api("/projects/#{project.id}/merge_requests", user), - title: "Test merge_request", target_branch: "master", author: user - expect(response).to have_gitlab_http_status(400) - end - - it "returns 400 when target_branch is missing" do - post v3_api("/projects/#{project.id}/merge_requests", user), - title: "Test merge_request", source_branch: "markdown", author: user - expect(response).to have_gitlab_http_status(400) - end - - it "returns 400 when title is missing" do - post v3_api("/projects/#{project.id}/merge_requests", user), - target_branch: 'master', source_branch: 'markdown' - expect(response).to have_gitlab_http_status(400) - end - - it 'allows special label names' do - post v3_api("/projects/#{project.id}/merge_requests", user), - title: 'Test merge_request', - source_branch: 'markdown', - target_branch: 'master', - author: user, - labels: 'label, label?, label&foo, ?, &' - expect(response.status).to eq(201) - expect(json_response['labels']).to include 'label' - expect(json_response['labels']).to include 'label?' - expect(json_response['labels']).to include 'label&foo' - expect(json_response['labels']).to include '?' - expect(json_response['labels']).to include '&' - end - - context 'with existing MR' do - before do - post v3_api("/projects/#{project.id}/merge_requests", user), - title: 'Test merge_request', - source_branch: 'feature_conflict', - target_branch: 'master', - author: user - @mr = MergeRequest.all.last - end - - it 'returns 409 when MR already exists for source/target' do - expect do - post v3_api("/projects/#{project.id}/merge_requests", user), - title: 'New test merge_request', - source_branch: 'feature_conflict', - target_branch: 'master', - author: user - end.to change { MergeRequest.count }.by(0) - expect(response).to have_gitlab_http_status(409) - end - end - end - - context 'forked projects' do - let!(:user2) { create(:user) } - let!(:forked_project) { fork_project(project, user2, repository: true) } - let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) } - - before do - forked_project.add_reporter(user2) - end - - it "returns merge_request" do - post v3_api("/projects/#{forked_project.id}/merge_requests", user2), - title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master", - author: user2, target_project_id: project.id, description: 'Test description for Test merge_request' - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('Test merge_request') - expect(json_response['description']).to eq('Test description for Test merge_request') - end - - it "does not return 422 when source_branch equals target_branch" do - expect(project.id).not_to eq(forked_project.id) - expect(forked_project.forked?).to be_truthy - expect(forked_project.forked_from_project).to eq(project) - post v3_api("/projects/#{forked_project.id}/merge_requests", user2), - title: 'Test merge_request', source_branch: "master", target_branch: "master", author: user2, target_project_id: project.id - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('Test merge_request') - end - - it "returns 403 when target project has disabled merge requests" do - project.project_feature.update(merge_requests_access_level: 0) - - post v3_api("/projects/#{forked_project.id}/merge_requests", user2), - title: 'Test', - target_branch: "master", - source_branch: 'markdown', - author: user2, - target_project_id: project.id - - expect(response).to have_gitlab_http_status(403) - end - - it "returns 400 when source_branch is missing" do - post v3_api("/projects/#{forked_project.id}/merge_requests", user2), - title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id - expect(response).to have_gitlab_http_status(400) - end - - it "returns 400 when target_branch is missing" do - post v3_api("/projects/#{forked_project.id}/merge_requests", user2), - title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id - expect(response).to have_gitlab_http_status(400) - end - - it "returns 400 when title is missing" do - post v3_api("/projects/#{forked_project.id}/merge_requests", user2), - target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id - expect(response).to have_gitlab_http_status(400) - end - - context 'when target_branch and target_project_id is specified' do - let(:params) do - { title: 'Test merge_request', - target_branch: 'master', - source_branch: 'markdown', - author: user2, - target_project_id: unrelated_project.id } - end - - it 'returns 422 if targeting a different fork' do - unrelated_project.add_developer(user2) - - post v3_api("/projects/#{forked_project.id}/merge_requests", user2), params - - expect(response).to have_gitlab_http_status(422) - end - - it 'returns 403 if targeting a different fork which user can not access' do - post v3_api("/projects/#{forked_project.id}/merge_requests", user2), params - - expect(response).to have_gitlab_http_status(403) - end - end - - it "returns 201 when target_branch is specified and for the same project" do - post v3_api("/projects/#{forked_project.id}/merge_requests", user2), - title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: forked_project.id - expect(response).to have_gitlab_http_status(201) - end - end - end - - describe "DELETE /projects/:id/merge_requests/:merge_request_id" do - context "when the user is developer" do - let(:developer) { create(:user) } - - before do - project.add_developer(developer) - end - - it "denies the deletion of the merge request" do - delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", developer) - expect(response).to have_gitlab_http_status(403) - end - end - - context "when the user is project owner" do - it "destroys the merge request owners can destroy" do - delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) - - expect(response).to have_gitlab_http_status(200) - end - end - end - - describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do - let(:pipeline) { create(:ci_pipeline_without_jobs) } - - it "returns merge_request in case of success" do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) - - expect(response).to have_gitlab_http_status(200) - end - - it "returns 406 if branch can't be merged" do - allow_any_instance_of(MergeRequest) - .to receive(:can_be_merged?).and_return(false) - - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) - - expect(response).to have_gitlab_http_status(406) - expect(json_response['message']).to eq('Branch cannot be merged') - end - - it "returns 405 if merge_request is not open" do - merge_request.close - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) - expect(response).to have_gitlab_http_status(405) - expect(json_response['message']).to eq('405 Method Not Allowed') - end - - it "returns 405 if merge_request is a work in progress" do - merge_request.update_attribute(:title, "WIP: #{merge_request.title}") - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) - expect(response).to have_gitlab_http_status(405) - expect(json_response['message']).to eq('405 Method Not Allowed') - end - - it 'returns 405 if the build failed for a merge request that requires success' do - allow_any_instance_of(MergeRequest).to receive(:mergeable_ci_state?).and_return(false) - - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) - - expect(response).to have_gitlab_http_status(405) - expect(json_response['message']).to eq('405 Method Not Allowed') - end - - it "returns 401 if user has no permissions to merge" do - user2 = create(:user) - project.add_reporter(user2) - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user2) - expect(response).to have_gitlab_http_status(401) - expect(json_response['message']).to eq('401 Unauthorized') - end - - it "returns 409 if the SHA parameter doesn't match" do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha.reverse - - expect(response).to have_gitlab_http_status(409) - expect(json_response['message']).to start_with('SHA does not match HEAD of source branch') - end - - it "succeeds if the SHA parameter matches" do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha - - expect(response).to have_gitlab_http_status(200) - end - - it "enables merge when pipeline succeeds if the pipeline is active" do - allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(pipeline) - allow(pipeline).to receive(:active?).and_return(true) - - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_build_succeeds: true - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq('Test') - expect(json_response['merge_when_build_succeeds']).to eq(true) - end - end - - describe "PUT /projects/:id/merge_requests/:merge_request_id" do - context "to close a MR" do - it "returns merge_request" do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close" - - expect(response).to have_gitlab_http_status(200) - expect(json_response['state']).to eq('closed') - end - end - - it "updates title and returns merge_request" do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), title: "New title" - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq('New title') - end - - it "updates description and returns merge_request" do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), description: "New description" - expect(response).to have_gitlab_http_status(200) - expect(json_response['description']).to eq('New description') - end - - it "updates milestone_id and returns merge_request" do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), milestone_id: milestone.id - expect(response).to have_gitlab_http_status(200) - expect(json_response['milestone']['id']).to eq(milestone.id) - end - - it "returns merge_request with renamed target_branch" do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), target_branch: "wiki" - expect(response).to have_gitlab_http_status(200) - expect(json_response['target_branch']).to eq('wiki') - end - - it "returns merge_request that removes the source branch" do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), remove_source_branch: true - - expect(response).to have_gitlab_http_status(200) - expect(json_response['force_remove_source_branch']).to be_truthy - end - - it 'allows special label names' do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), - title: 'new issue', - labels: 'label, label?, label&foo, ?, &' - - expect(response.status).to eq(200) - expect(json_response['labels']).to include 'label' - expect(json_response['labels']).to include 'label?' - expect(json_response['labels']).to include 'label&foo' - expect(json_response['labels']).to include '?' - expect(json_response['labels']).to include '&' - end - - it 'does not update state when title is empty' do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', title: nil - - merge_request.reload - expect(response).to have_gitlab_http_status(400) - expect(merge_request.state).to eq('opened') - end - - it 'does not update state when target_branch is empty' do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', target_branch: nil - - merge_request.reload - expect(response).to have_gitlab_http_status(400) - expect(merge_request.state).to eq('opened') - end - end - - describe "POST /projects/:id/merge_requests/:merge_request_id/comments" do - it "returns comment" do - original_count = merge_request.notes.size - - post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), note: "My comment" - - expect(response).to have_gitlab_http_status(201) - expect(json_response['note']).to eq('My comment') - expect(json_response['author']['name']).to eq(user.name) - expect(json_response['author']['username']).to eq(user.username) - expect(merge_request.reload.notes.size).to eq(original_count + 1) - end - - it "returns 400 if note is missing" do - post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user) - expect(response).to have_gitlab_http_status(400) - end - - it "returns 404 if note is attached to non existent merge request" do - post v3_api("/projects/#{project.id}/merge_requests/404/comments", user), - note: 'My comment' - expect(response).to have_gitlab_http_status(404) - end - end - - describe "GET :id/merge_requests/:merge_request_id/comments" do - let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") } - let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") } - - it "returns merge_request comments ordered by created_at" do - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - expect(json_response.first['note']).to eq("a comment on a MR") - expect(json_response.first['author']['id']).to eq(user.id) - expect(json_response.last['note']).to eq("another comment on a MR") - end - - it "returns a 404 error if merge_request_id not found" do - get v3_api("/projects/#{project.id}/merge_requests/999/comments", user) - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'GET :id/merge_requests/:merge_request_id/closes_issues' do - it 'returns the issue that will be closed on merge' do - issue = create(:issue, project: project) - mr = merge_request.tap do |mr| - mr.update_attribute(:description, "Closes #{issue.to_reference(mr.project)}") - end - - get v3_api("/projects/#{project.id}/merge_requests/#{mr.id}/closes_issues", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(issue.id) - end - - it 'returns an empty array when there are no issues to be closed' do - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) - end - - it 'handles external issues' do - jira_project = create(:jira_project, :public, :repository, name: 'JIR_EXT1') - issue = ExternalIssue.new("#{jira_project.name}-123", jira_project) - merge_request = create(:merge_request, :simple, author: user, assignee: user, source_project: jira_project) - merge_request.update_attribute(:description, "Closes #{issue.to_reference(jira_project)}") - - get v3_api("/projects/#{jira_project.id}/merge_requests/#{merge_request.id}/closes_issues", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['title']).to eq(issue.title) - expect(json_response.first['id']).to eq(issue.id) - end - - it 'returns 403 if the user has no access to the merge request' do - project = create(:project, :private, :repository) - merge_request = create(:merge_request, :simple, source_project: project) - guest = create(:user) - project.add_guest(guest) - - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", guest) - - expect(response).to have_gitlab_http_status(403) - end - end - - describe 'POST :id/merge_requests/:merge_request_id/subscription' do - it 'subscribes to a merge request' do - post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin) - - expect(response).to have_gitlab_http_status(201) - expect(json_response['subscribed']).to eq(true) - end - - it 'returns 304 if already subscribed' do - post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user) - - expect(response).to have_gitlab_http_status(304) - end - - it 'returns 404 if the merge request is not found' do - post v3_api("/projects/#{project.id}/merge_requests/123/subscription", user) - - expect(response).to have_gitlab_http_status(404) - end - - it 'returns 403 if user has no access to read code' do - guest = create(:user) - project.add_guest(guest) - - post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest) - - expect(response).to have_gitlab_http_status(403) - end - end - - describe 'DELETE :id/merge_requests/:merge_request_id/subscription' do - it 'unsubscribes from a merge request' do - delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['subscribed']).to eq(false) - end - - it 'returns 304 if not subscribed' do - delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin) - - expect(response).to have_gitlab_http_status(304) - end - - it 'returns 404 if the merge request is not found' do - post v3_api("/projects/#{project.id}/merge_requests/123/subscription", user) - - expect(response).to have_gitlab_http_status(404) - end - - it 'returns 403 if user has no access to read code' do - guest = create(:user) - project.add_guest(guest) - - delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest) - - expect(response).to have_gitlab_http_status(403) - end - end - - describe 'Time tracking' do - let(:issuable) { merge_request } - - include_examples 'V3 time tracking endpoints', 'merge_request' - end - - def mr_with_later_created_and_updated_at_time - merge_request - merge_request.created_at += 1.hour - merge_request.updated_at += 30.minutes - merge_request.save - merge_request - end - - def mr_with_earlier_created_and_updated_at_time - merge_request_closed - merge_request_closed.created_at -= 1.hour - merge_request_closed.updated_at -= 30.minutes - merge_request_closed.save - merge_request_closed - end -end diff --git a/spec/requests/api/v3/milestones_spec.rb b/spec/requests/api/v3/milestones_spec.rb deleted file mode 100644 index 6021600e09c..00000000000 --- a/spec/requests/api/v3/milestones_spec.rb +++ /dev/null @@ -1,238 +0,0 @@ -require 'spec_helper' - -describe API::V3::Milestones do - let(:user) { create(:user) } - let!(:project) { create(:project, namespace: user.namespace ) } - let!(:closed_milestone) { create(:closed_milestone, project: project) } - let!(:milestone) { create(:milestone, project: project) } - - before { project.add_developer(user) } - - describe 'GET /projects/:id/milestones' do - it 'returns project milestones' do - get v3_api("/projects/#{project.id}/milestones", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['title']).to eq(milestone.title) - end - - it 'returns a 401 error if user not authenticated' do - get v3_api("/projects/#{project.id}/milestones") - - expect(response).to have_gitlab_http_status(401) - end - - it 'returns an array of active milestones' do - get v3_api("/projects/#{project.id}/milestones?state=active", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(milestone.id) - end - - it 'returns an array of closed milestones' do - get v3_api("/projects/#{project.id}/milestones?state=closed", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(closed_milestone.id) - end - end - - describe 'GET /projects/:id/milestones/:milestone_id' do - it 'returns a project milestone by id' do - get v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq(milestone.title) - expect(json_response['iid']).to eq(milestone.iid) - end - - it 'returns a project milestone by iid' do - get v3_api("/projects/#{project.id}/milestones?iid=#{closed_milestone.iid}", user) - - expect(response.status).to eq 200 - expect(json_response.size).to eq(1) - expect(json_response.first['title']).to eq closed_milestone.title - expect(json_response.first['id']).to eq closed_milestone.id - end - - it 'returns a project milestone by iid array' do - get v3_api("/projects/#{project.id}/milestones", user), iid: [milestone.iid, closed_milestone.iid] - - expect(response).to have_gitlab_http_status(200) - expect(json_response.size).to eq(2) - expect(json_response.first['title']).to eq milestone.title - expect(json_response.first['id']).to eq milestone.id - end - - it 'returns 401 error if user not authenticated' do - get v3_api("/projects/#{project.id}/milestones/#{milestone.id}") - - expect(response).to have_gitlab_http_status(401) - end - - it 'returns a 404 error if milestone id not found' do - get v3_api("/projects/#{project.id}/milestones/1234", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'POST /projects/:id/milestones' do - it 'creates a new project milestone' do - post v3_api("/projects/#{project.id}/milestones", user), title: 'new milestone' - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('new milestone') - expect(json_response['description']).to be_nil - end - - it 'creates a new project milestone with description and dates' do - post v3_api("/projects/#{project.id}/milestones", user), - title: 'new milestone', description: 'release', due_date: '2013-03-02', start_date: '2013-02-02' - - expect(response).to have_gitlab_http_status(201) - expect(json_response['description']).to eq('release') - expect(json_response['due_date']).to eq('2013-03-02') - expect(json_response['start_date']).to eq('2013-02-02') - end - - it 'returns a 400 error if title is missing' do - post v3_api("/projects/#{project.id}/milestones", user) - - expect(response).to have_gitlab_http_status(400) - end - - it 'returns a 400 error if params are invalid (duplicate title)' do - post v3_api("/projects/#{project.id}/milestones", user), - title: milestone.title, description: 'release', due_date: '2013-03-02' - - expect(response).to have_gitlab_http_status(400) - end - - it 'creates a new project with reserved html characters' do - post v3_api("/projects/#{project.id}/milestones", user), title: 'foo & bar 1.1 -> 2.2' - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('foo & bar 1.1 -> 2.2') - expect(json_response['description']).to be_nil - end - end - - describe 'PUT /projects/:id/milestones/:milestone_id' do - it 'updates a project milestone' do - put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user), - title: 'updated title' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq('updated title') - end - - it 'removes a due date if nil is passed' do - milestone.update!(due_date: "2016-08-05") - - put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user), due_date: nil - - expect(response).to have_gitlab_http_status(200) - expect(json_response['due_date']).to be_nil - end - - it 'returns a 404 error if milestone id not found' do - put v3_api("/projects/#{project.id}/milestones/1234", user), - title: 'updated title' - - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'PUT /projects/:id/milestones/:milestone_id to close milestone' do - it 'updates a project milestone' do - put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user), - state_event: 'close' - expect(response).to have_gitlab_http_status(200) - - expect(json_response['state']).to eq('closed') - end - end - - describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do - it 'creates an activity event when an milestone is closed' do - expect(Event).to receive(:create!) - - put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user), - state_event: 'close' - end - end - - describe 'GET /projects/:id/milestones/:milestone_id/issues' do - before do - milestone.issues << create(:issue, project: project) - end - it 'returns project issues for a particular milestone' do - get v3_api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['milestone']['title']).to eq(milestone.title) - end - - it 'matches V3 response schema for a list of issues' do - get v3_api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to match_response_schema('public_api/v3/issues') - end - - it 'returns a 401 error if user not authenticated' do - get v3_api("/projects/#{project.id}/milestones/#{milestone.id}/issues") - - expect(response).to have_gitlab_http_status(401) - end - - describe 'confidential issues' do - let(:public_project) { create(:project, :public) } - let(:milestone) { create(:milestone, project: public_project) } - let(:issue) { create(:issue, project: public_project) } - let(:confidential_issue) { create(:issue, confidential: true, project: public_project) } - - before do - public_project.add_developer(user) - milestone.issues << issue << confidential_issue - end - - it 'returns confidential issues to team members' do - get v3_api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to eq(2) - expect(json_response.map { |issue| issue['id'] }).to include(issue.id, confidential_issue.id) - end - - it 'does not return confidential issues to team members with guest role' do - member = create(:user) - project.add_guest(member) - - get v3_api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", member) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to eq(1) - expect(json_response.map { |issue| issue['id'] }).to include(issue.id) - end - - it 'does not return confidential issues to regular users' do - get v3_api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", create(:user)) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to eq(1) - expect(json_response.map { |issue| issue['id'] }).to include(issue.id) - end - end - end -end diff --git a/spec/requests/api/v3/notes_spec.rb b/spec/requests/api/v3/notes_spec.rb deleted file mode 100644 index 5532795ab02..00000000000 --- a/spec/requests/api/v3/notes_spec.rb +++ /dev/null @@ -1,431 +0,0 @@ -require 'spec_helper' - -describe API::V3::Notes do - let(:user) { create(:user) } - let!(:project) { create(:project, :public, namespace: user.namespace) } - let!(:issue) { create(:issue, project: project, author: user) } - let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) } - let!(:snippet) { create(:project_snippet, project: project, author: user) } - let!(:issue_note) { create(:note, noteable: issue, project: project, author: user) } - let!(:merge_request_note) { create(:note, noteable: merge_request, project: project, author: user) } - let!(:snippet_note) { create(:note, noteable: snippet, project: project, author: user) } - - # For testing the cross-reference of a private issue in a public issue - let(:private_user) { create(:user) } - let(:private_project) do - create(:project, namespace: private_user.namespace) - .tap { |p| p.add_master(private_user) } - end - let(:private_issue) { create(:issue, project: private_project) } - - let(:ext_proj) { create(:project, :public) } - let(:ext_issue) { create(:issue, project: ext_proj) } - - let!(:cross_reference_note) do - create :note, - noteable: ext_issue, project: ext_proj, - note: "mentioned in issue #{private_issue.to_reference(ext_proj)}", - system: true - end - - before { project.add_reporter(user) } - - describe "GET /projects/:id/noteable/:noteable_id/notes" do - context "when noteable is an Issue" do - it "returns an array of issue notes" do - get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.first['body']).to eq(issue_note.note) - expect(json_response.first['upvote']).to be_falsey - expect(json_response.first['downvote']).to be_falsey - end - - it "returns a 404 error when issue id not found" do - get v3_api("/projects/#{project.id}/issues/12345/notes", user) - - expect(response).to have_gitlab_http_status(404) - end - - context "and current user cannot view the notes" do - it "returns an empty array" do - get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response).to be_empty - end - - context "and issue is confidential" do - before { ext_issue.update_attributes(confidential: true) } - - it "returns 404" do - get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - context "and current user can view the note" do - it "returns an empty array" do - get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", private_user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.first['body']).to eq(cross_reference_note.note) - end - end - end - end - - context "when noteable is a Snippet" do - it "returns an array of snippet notes" do - get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.first['body']).to eq(snippet_note.note) - end - - it "returns a 404 error when snippet id not found" do - get v3_api("/projects/#{project.id}/snippets/42/notes", user) - - expect(response).to have_gitlab_http_status(404) - end - - it "returns 404 when not authorized" do - get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", private_user) - - expect(response).to have_gitlab_http_status(404) - end - end - - context "when noteable is a Merge Request" do - it "returns an array of merge_requests notes" do - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes", user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.first['body']).to eq(merge_request_note.note) - end - - it "returns a 404 error if merge request id not found" do - get v3_api("/projects/#{project.id}/merge_requests/4444/notes", user) - - expect(response).to have_gitlab_http_status(404) - end - - it "returns 404 when not authorized" do - get v3_api("/projects/#{project.id}/merge_requests/4444/notes", private_user) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe "GET /projects/:id/noteable/:noteable_id/notes/:note_id" do - context "when noteable is an Issue" do - it "returns an issue note by id" do - get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['body']).to eq(issue_note.note) - end - - it "returns a 404 error if issue note not found" do - get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user) - - expect(response).to have_gitlab_http_status(404) - end - - context "and current user cannot view the note" do - it "returns a 404 error" do - get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", user) - - expect(response).to have_gitlab_http_status(404) - end - - context "when issue is confidential" do - before { issue.update_attributes(confidential: true) } - - it "returns 404" do - get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", private_user) - - expect(response).to have_gitlab_http_status(404) - end - end - - context "and current user can view the note" do - it "returns an issue note by id" do - get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", private_user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['body']).to eq(cross_reference_note.note) - end - end - end - end - - context "when noteable is a Snippet" do - it "returns a snippet note by id" do - get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes/#{snippet_note.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['body']).to eq(snippet_note.note) - end - - it "returns a 404 error if snippet note not found" do - get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes/12345", user) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe "POST /projects/:id/noteable/:noteable_id/notes" do - context "when noteable is an Issue" do - it "creates a new issue note" do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!' - - expect(response).to have_gitlab_http_status(201) - expect(json_response['body']).to eq('hi!') - expect(json_response['author']['username']).to eq(user.username) - end - - it "returns a 400 bad request error if body not given" do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user) - - expect(response).to have_gitlab_http_status(400) - end - - it "returns a 401 unauthorized error if user not authenticated" do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!' - - expect(response).to have_gitlab_http_status(401) - end - - context 'when an admin or owner makes the request' do - it 'accepts the creation date to be set' do - creation_time = 2.weeks.ago - post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), - body: 'hi!', created_at: creation_time - - expect(response).to have_gitlab_http_status(201) - expect(json_response['body']).to eq('hi!') - expect(json_response['author']['username']).to eq(user.username) - expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time) - end - end - - context 'when the user is posting an award emoji on an issue created by someone else' do - let(:issue2) { create(:issue, project: project) } - - it 'creates a new issue note' do - post v3_api("/projects/#{project.id}/issues/#{issue2.id}/notes", user), body: ':+1:' - - expect(response).to have_gitlab_http_status(201) - expect(json_response['body']).to eq(':+1:') - end - end - - context 'when the user is posting an award emoji on his/her own issue' do - it 'creates a new issue note' do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: ':+1:' - - expect(response).to have_gitlab_http_status(201) - expect(json_response['body']).to eq(':+1:') - end - end - end - - context "when noteable is a Snippet" do - it "creates a new snippet note" do - post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user), body: 'hi!' - - expect(response).to have_gitlab_http_status(201) - expect(json_response['body']).to eq('hi!') - expect(json_response['author']['username']).to eq(user.username) - end - - it "returns a 400 bad request error if body not given" do - post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user) - - expect(response).to have_gitlab_http_status(400) - end - - it "returns a 401 unauthorized error if user not authenticated" do - post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes"), body: 'hi!' - - expect(response).to have_gitlab_http_status(401) - end - end - - context 'when user does not have access to read the noteable' do - it 'responds with 404' do - project = create(:project, :private) { |p| p.add_guest(user) } - issue = create(:issue, :confidential, project: project) - - post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), - body: 'Foo' - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'when user does not have access to create noteable' do - let(:private_issue) { create(:issue, project: create(:project, :private)) } - - ## - # We are posting to project user has access to, but we use issue id - # from a different project, see #15577 - # - before do - post v3_api("/projects/#{project.id}/issues/#{private_issue.id}/notes", user), - body: 'Hi!' - end - - it 'responds with resource not found error' do - expect(response.status).to eq 404 - end - - it 'does not create new note' do - expect(private_issue.notes.reload).to be_empty - end - end - end - - describe "POST /projects/:id/noteable/:noteable_id/notes to test observer on create" do - it "creates an activity event when an issue note is created" do - expect(Event).to receive(:create!) - - post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!' - end - end - - describe 'PUT /projects/:id/noteable/:noteable_id/notes/:note_id' do - context 'when noteable is an Issue' do - it 'returns modified note' do - put v3_api("/projects/#{project.id}/issues/#{issue.id}/"\ - "notes/#{issue_note.id}", user), body: 'Hello!' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['body']).to eq('Hello!') - end - - it 'returns a 404 error when note id not found' do - put v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user), - body: 'Hello!' - - expect(response).to have_gitlab_http_status(404) - end - - it 'returns a 400 bad request error if body not given' do - put v3_api("/projects/#{project.id}/issues/#{issue.id}/"\ - "notes/#{issue_note.id}", user) - - expect(response).to have_gitlab_http_status(400) - end - end - - context 'when noteable is a Snippet' do - it 'returns modified note' do - put v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ - "notes/#{snippet_note.id}", user), body: 'Hello!' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['body']).to eq('Hello!') - end - - it 'returns a 404 error when note id not found' do - put v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ - "notes/12345", user), body: "Hello!" - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'when noteable is a Merge Request' do - it 'returns modified note' do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ - "notes/#{merge_request_note.id}", user), body: 'Hello!' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['body']).to eq('Hello!') - end - - it 'returns a 404 error when note id not found' do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ - "notes/12345", user), body: "Hello!" - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe 'DELETE /projects/:id/noteable/:noteable_id/notes/:note_id' do - context 'when noteable is an Issue' do - it 'deletes a note' do - delete v3_api("/projects/#{project.id}/issues/#{issue.id}/"\ - "notes/#{issue_note.id}", user) - - expect(response).to have_gitlab_http_status(200) - # Check if note is really deleted - delete v3_api("/projects/#{project.id}/issues/#{issue.id}/"\ - "notes/#{issue_note.id}", user) - expect(response).to have_gitlab_http_status(404) - end - - it 'returns a 404 error when note id not found' do - delete v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'when noteable is a Snippet' do - it 'deletes a note' do - delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ - "notes/#{snippet_note.id}", user) - - expect(response).to have_gitlab_http_status(200) - # Check if note is really deleted - delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ - "notes/#{snippet_note.id}", user) - expect(response).to have_gitlab_http_status(404) - end - - it 'returns a 404 error when note id not found' do - delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ - "notes/12345", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'when noteable is a Merge Request' do - it 'deletes a note' do - delete v3_api("/projects/#{project.id}/merge_requests/"\ - "#{merge_request.id}/notes/#{merge_request_note.id}", user) - - expect(response).to have_gitlab_http_status(200) - # Check if note is really deleted - delete v3_api("/projects/#{project.id}/merge_requests/"\ - "#{merge_request.id}/notes/#{merge_request_note.id}", user) - expect(response).to have_gitlab_http_status(404) - end - - it 'returns a 404 error when note id not found' do - delete v3_api("/projects/#{project.id}/merge_requests/"\ - "#{merge_request.id}/notes/12345", user) - - expect(response).to have_gitlab_http_status(404) - end - end - end -end diff --git a/spec/requests/api/v3/pipelines_spec.rb b/spec/requests/api/v3/pipelines_spec.rb deleted file mode 100644 index ea943f22c41..00000000000 --- a/spec/requests/api/v3/pipelines_spec.rb +++ /dev/null @@ -1,201 +0,0 @@ -require 'spec_helper' - -describe API::V3::Pipelines do - let(:user) { create(:user) } - let(:non_member) { create(:user) } - let(:project) { create(:project, :repository, creator: user) } - - let!(:pipeline) do - create(:ci_empty_pipeline, project: project, sha: project.commit.id, - ref: project.default_branch) - end - - before { project.add_master(user) } - - shared_examples 'a paginated resources' do - before do - # Fires the request - request - end - - it 'has pagination headers' do - expect(response).to include_pagination_headers - end - end - - describe 'GET /projects/:id/pipelines ' do - it_behaves_like 'a paginated resources' do - let(:request) { get v3_api("/projects/#{project.id}/pipelines", user) } - end - - context 'authorized user' do - it 'returns project pipelines' do - get v3_api("/projects/#{project.id}/pipelines", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['sha']).to match(/\A\h{40}\z/) - expect(json_response.first['id']).to eq pipeline.id - expect(json_response.first.keys).to contain_exactly(*%w[id sha ref status before_sha tag yaml_errors user created_at updated_at started_at finished_at committed_at duration coverage]) - end - end - - context 'unauthorized user' do - it 'does not return project pipelines' do - get v3_api("/projects/#{project.id}/pipelines", non_member) - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq '404 Project Not Found' - expect(json_response).not_to be_an Array - end - end - end - - describe 'POST /projects/:id/pipeline ' do - context 'authorized user' do - context 'with gitlab-ci.yml' do - before { stub_ci_pipeline_to_return_yaml_file } - - it 'creates and returns a new pipeline' do - expect do - post v3_api("/projects/#{project.id}/pipeline", user), ref: project.default_branch - end.to change { Ci::Pipeline.count }.by(1) - - expect(response).to have_gitlab_http_status(201) - expect(json_response).to be_a Hash - expect(json_response['sha']).to eq project.commit.id - end - - it 'fails when using an invalid ref' do - post v3_api("/projects/#{project.id}/pipeline", user), ref: 'invalid_ref' - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']['base'].first).to eq 'Reference not found' - expect(json_response).not_to be_an Array - end - end - - context 'without gitlab-ci.yml' do - it 'fails to create pipeline' do - post v3_api("/projects/#{project.id}/pipeline", user), ref: project.default_branch - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']['base'].first).to eq 'Missing .gitlab-ci.yml file' - expect(json_response).not_to be_an Array - end - end - end - - context 'unauthorized user' do - it 'does not create pipeline' do - post v3_api("/projects/#{project.id}/pipeline", non_member), ref: project.default_branch - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq '404 Project Not Found' - expect(json_response).not_to be_an Array - end - end - end - - describe 'GET /projects/:id/pipelines/:pipeline_id' do - context 'authorized user' do - it 'returns project pipelines' do - get v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['sha']).to match /\A\h{40}\z/ - end - - it 'returns 404 when it does not exist' do - get v3_api("/projects/#{project.id}/pipelines/123456", user) - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq '404 Not found' - expect(json_response['id']).to be nil - end - - context 'with coverage' do - before do - create(:ci_build, coverage: 30, pipeline: pipeline) - end - - it 'exposes the coverage' do - get v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}", user) - - expect(json_response["coverage"].to_i).to eq(30) - end - end - end - - context 'unauthorized user' do - it 'should not return a project pipeline' do - get v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member) - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq '404 Project Not Found' - expect(json_response['id']).to be nil - end - end - end - - describe 'POST /projects/:id/pipelines/:pipeline_id/retry' do - context 'authorized user' do - let!(:pipeline) do - create(:ci_pipeline, project: project, sha: project.commit.id, - ref: project.default_branch) - end - - let!(:build) { create(:ci_build, :failed, pipeline: pipeline) } - - it 'retries failed builds' do - expect do - post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", user) - end.to change { pipeline.builds.count }.from(1).to(2) - - expect(response).to have_gitlab_http_status(201) - expect(build.reload.retried?).to be true - end - end - - context 'unauthorized user' do - it 'should not return a project pipeline' do - post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", non_member) - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq '404 Project Not Found' - expect(json_response['id']).to be nil - end - end - end - - describe 'POST /projects/:id/pipelines/:pipeline_id/cancel' do - let!(:pipeline) do - create(:ci_empty_pipeline, project: project, sha: project.commit.id, - ref: project.default_branch) - end - - let!(:build) { create(:ci_build, :running, pipeline: pipeline) } - - context 'authorized user' do - it 'retries failed builds' do - post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['status']).to eq('canceled') - end - end - - context 'user without proper access rights' do - let!(:reporter) { create(:user) } - - before { project.add_reporter(reporter) } - - it 'rejects the action' do - post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", reporter) - - expect(response).to have_gitlab_http_status(403) - expect(pipeline.reload.status).to eq('pending') - end - end - end -end diff --git a/spec/requests/api/v3/project_hooks_spec.rb b/spec/requests/api/v3/project_hooks_spec.rb deleted file mode 100644 index 8f6a2330d25..00000000000 --- a/spec/requests/api/v3/project_hooks_spec.rb +++ /dev/null @@ -1,219 +0,0 @@ -require 'spec_helper' - -describe API::ProjectHooks, 'ProjectHooks' do - let(:user) { create(:user) } - let(:user3) { create(:user) } - let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } - let!(:hook) do - create(:project_hook, - :all_events_enabled, - project: project, - url: 'http://example.com', - enable_ssl_verification: true) - end - - before do - project.add_master(user) - project.add_developer(user3) - end - - describe "GET /projects/:id/hooks" do - context "authorized user" do - it "returns project hooks" do - get v3_api("/projects/#{project.id}/hooks", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.count).to eq(1) - expect(json_response.first['url']).to eq("http://example.com") - expect(json_response.first['issues_events']).to eq(true) - expect(json_response.first['confidential_issues_events']).to eq(true) - expect(json_response.first['push_events']).to eq(true) - expect(json_response.first['merge_requests_events']).to eq(true) - expect(json_response.first['tag_push_events']).to eq(true) - expect(json_response.first['note_events']).to eq(true) - expect(json_response.first['build_events']).to eq(true) - expect(json_response.first['pipeline_events']).to eq(true) - expect(json_response.first['wiki_page_events']).to eq(true) - expect(json_response.first['enable_ssl_verification']).to eq(true) - end - end - - context "unauthorized user" do - it "does not access project hooks" do - get v3_api("/projects/#{project.id}/hooks", user3) - - expect(response).to have_gitlab_http_status(403) - end - end - end - - describe "GET /projects/:id/hooks/:hook_id" do - context "authorized user" do - it "returns a project hook" do - get v3_api("/projects/#{project.id}/hooks/#{hook.id}", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response['url']).to eq(hook.url) - expect(json_response['issues_events']).to eq(hook.issues_events) - expect(json_response['confidential_issues_events']).to eq(hook.confidential_issues_events) - expect(json_response['push_events']).to eq(hook.push_events) - expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) - expect(json_response['tag_push_events']).to eq(hook.tag_push_events) - expect(json_response['note_events']).to eq(hook.note_events) - expect(json_response['build_events']).to eq(hook.job_events) - expect(json_response['pipeline_events']).to eq(hook.pipeline_events) - expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events) - expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification) - end - - it "returns a 404 error if hook id is not available" do - get v3_api("/projects/#{project.id}/hooks/1234", user) - expect(response).to have_gitlab_http_status(404) - end - end - - context "unauthorized user" do - it "does not access an existing hook" do - get v3_api("/projects/#{project.id}/hooks/#{hook.id}", user3) - expect(response).to have_gitlab_http_status(403) - end - end - - it "returns a 404 error if hook id is not available" do - get v3_api("/projects/#{project.id}/hooks/1234", user) - expect(response).to have_gitlab_http_status(404) - end - end - - describe "POST /projects/:id/hooks" do - it "adds hook to project" do - expect do - post v3_api("/projects/#{project.id}/hooks", user), - url: "http://example.com", issues_events: true, confidential_issues_events: true, wiki_page_events: true, build_events: true - end.to change {project.hooks.count}.by(1) - - expect(response).to have_gitlab_http_status(201) - expect(json_response['url']).to eq('http://example.com') - expect(json_response['issues_events']).to eq(true) - expect(json_response['confidential_issues_events']).to eq(true) - expect(json_response['push_events']).to eq(true) - expect(json_response['merge_requests_events']).to eq(false) - expect(json_response['tag_push_events']).to eq(false) - expect(json_response['note_events']).to eq(false) - expect(json_response['build_events']).to eq(true) - expect(json_response['pipeline_events']).to eq(false) - expect(json_response['wiki_page_events']).to eq(true) - expect(json_response['enable_ssl_verification']).to eq(true) - expect(json_response).not_to include('token') - end - - it "adds the token without including it in the response" do - token = "secret token" - - expect do - post v3_api("/projects/#{project.id}/hooks", user), url: "http://example.com", token: token - end.to change {project.hooks.count}.by(1) - - expect(response).to have_gitlab_http_status(201) - expect(json_response["url"]).to eq("http://example.com") - expect(json_response).not_to include("token") - - hook = project.hooks.find(json_response["id"]) - - expect(hook.url).to eq("http://example.com") - expect(hook.token).to eq(token) - end - - it "returns a 400 error if url not given" do - post v3_api("/projects/#{project.id}/hooks", user) - expect(response).to have_gitlab_http_status(400) - end - - it "returns a 422 error if url not valid" do - post v3_api("/projects/#{project.id}/hooks", user), "url" => "ftp://example.com" - expect(response).to have_gitlab_http_status(422) - end - end - - describe "PUT /projects/:id/hooks/:hook_id" do - it "updates an existing project hook" do - put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user), - url: 'http://example.org', push_events: false, build_events: true - expect(response).to have_gitlab_http_status(200) - expect(json_response['url']).to eq('http://example.org') - expect(json_response['issues_events']).to eq(hook.issues_events) - expect(json_response['confidential_issues_events']).to eq(hook.confidential_issues_events) - expect(json_response['push_events']).to eq(false) - expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) - expect(json_response['tag_push_events']).to eq(hook.tag_push_events) - expect(json_response['note_events']).to eq(hook.note_events) - expect(json_response['build_events']).to eq(hook.job_events) - expect(json_response['pipeline_events']).to eq(hook.pipeline_events) - expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events) - expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification) - end - - it "adds the token without including it in the response" do - token = "secret token" - - put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user), url: "http://example.org", token: token - - expect(response).to have_gitlab_http_status(200) - expect(json_response["url"]).to eq("http://example.org") - expect(json_response).not_to include("token") - - expect(hook.reload.url).to eq("http://example.org") - expect(hook.reload.token).to eq(token) - end - - it "returns 404 error if hook id not found" do - put v3_api("/projects/#{project.id}/hooks/1234", user), url: 'http://example.org' - expect(response).to have_gitlab_http_status(404) - end - - it "returns 400 error if url is not given" do - put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user) - expect(response).to have_gitlab_http_status(400) - end - - it "returns a 422 error if url is not valid" do - put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user), url: 'ftp://example.com' - expect(response).to have_gitlab_http_status(422) - end - end - - describe "DELETE /projects/:id/hooks/:hook_id" do - it "deletes hook from project" do - expect do - delete v3_api("/projects/#{project.id}/hooks/#{hook.id}", user) - end.to change {project.hooks.count}.by(-1) - expect(response).to have_gitlab_http_status(200) - end - - it "returns success when deleting hook" do - delete v3_api("/projects/#{project.id}/hooks/#{hook.id}", user) - expect(response).to have_gitlab_http_status(200) - end - - it "returns a 404 error when deleting non existent hook" do - delete v3_api("/projects/#{project.id}/hooks/42", user) - expect(response).to have_gitlab_http_status(404) - end - - it "returns a 404 error if hook id not given" do - delete v3_api("/projects/#{project.id}/hooks", user) - - expect(response).to have_gitlab_http_status(404) - end - - it "returns a 404 if a user attempts to delete project hooks he/she does not own" do - test_user = create(:user) - other_project = create(:project) - other_project.add_master(test_user) - - delete v3_api("/projects/#{other_project.id}/hooks/#{hook.id}", test_user) - expect(response).to have_gitlab_http_status(404) - expect(WebHook.exists?(hook.id)).to be_truthy - end - end -end diff --git a/spec/requests/api/v3/project_snippets_spec.rb b/spec/requests/api/v3/project_snippets_spec.rb deleted file mode 100644 index 2ed31b99516..00000000000 --- a/spec/requests/api/v3/project_snippets_spec.rb +++ /dev/null @@ -1,226 +0,0 @@ -require 'rails_helper' - -describe API::ProjectSnippets do - let(:project) { create(:project, :public) } - let(:user) { create(:user) } - let(:admin) { create(:admin) } - - describe 'GET /projects/:project_id/snippets/:id' do - # TODO (rspeicher): Deprecated; remove in 9.0 - it 'always exposes expires_at as nil' do - snippet = create(:project_snippet, author: admin) - - get v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin) - - expect(json_response).to have_key('expires_at') - expect(json_response['expires_at']).to be_nil - end - end - - describe 'GET /projects/:project_id/snippets/' do - let(:user) { create(:user) } - - it 'returns all snippets available to team member' do - project.add_developer(user) - public_snippet = create(:project_snippet, :public, project: project) - internal_snippet = create(:project_snippet, :internal, project: project) - private_snippet = create(:project_snippet, :private, project: project) - - get v3_api("/projects/#{project.id}/snippets/", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response.size).to eq(3) - expect(json_response.map { |snippet| snippet['id']} ).to include(public_snippet.id, internal_snippet.id, private_snippet.id) - expect(json_response.last).to have_key('web_url') - end - - it 'hides private snippets from regular user' do - create(:project_snippet, :private, project: project) - - get v3_api("/projects/#{project.id}/snippets/", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response.size).to eq(0) - end - end - - describe 'POST /projects/:project_id/snippets/' do - let(:params) do - { - title: 'Test Title', - file_name: 'test.rb', - code: 'puts "hello world"', - visibility_level: Snippet::PUBLIC - } - end - - it 'creates a new snippet' do - post v3_api("/projects/#{project.id}/snippets/", admin), params - - expect(response).to have_gitlab_http_status(201) - snippet = ProjectSnippet.find(json_response['id']) - expect(snippet.content).to eq(params[:code]) - expect(snippet.title).to eq(params[:title]) - expect(snippet.file_name).to eq(params[:file_name]) - expect(snippet.visibility_level).to eq(params[:visibility_level]) - end - - it 'returns 400 for missing parameters' do - params.delete(:title) - - post v3_api("/projects/#{project.id}/snippets/", admin), params - - expect(response).to have_gitlab_http_status(400) - end - - context 'when the snippet is spam' do - def create_snippet(project, snippet_params = {}) - project.add_developer(user) - - post v3_api("/projects/#{project.id}/snippets", user), params.merge(snippet_params) - end - - before do - allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true) - end - - context 'when the snippet is private' do - it 'creates the snippet' do - expect { create_snippet(project, visibility_level: Snippet::PRIVATE) } - .to change { Snippet.count }.by(1) - end - end - - context 'when the snippet is public' do - it 'rejects the shippet' do - expect { create_snippet(project, visibility_level: Snippet::PUBLIC) } - .not_to change { Snippet.count } - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']).to eq({ "error" => "Spam detected" }) - end - - it 'creates a spam log' do - expect { create_snippet(project, visibility_level: Snippet::PUBLIC) } - .to change { SpamLog.count }.by(1) - end - end - end - end - - describe 'PUT /projects/:project_id/snippets/:id/' do - let(:visibility_level) { Snippet::PUBLIC } - let(:snippet) { create(:project_snippet, author: admin, visibility_level: visibility_level) } - - it 'updates snippet' do - new_content = 'New content' - - put v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin), code: new_content - - expect(response).to have_gitlab_http_status(200) - snippet.reload - expect(snippet.content).to eq(new_content) - end - - it 'returns 404 for invalid snippet id' do - put v3_api("/projects/#{snippet.project.id}/snippets/1234", admin), title: 'foo' - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Snippet Not Found') - end - - it 'returns 400 for missing parameters' do - put v3_api("/projects/#{project.id}/snippets/1234", admin) - - expect(response).to have_gitlab_http_status(400) - end - - context 'when the snippet is spam' do - def update_snippet(snippet_params = {}) - put v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin), snippet_params - end - - before do - allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true) - end - - context 'when the snippet is private' do - let(:visibility_level) { Snippet::PRIVATE } - - it 'creates the snippet' do - expect { update_snippet(title: 'Foo') } - .to change { snippet.reload.title }.to('Foo') - end - end - - context 'when the snippet is public' do - let(:visibility_level) { Snippet::PUBLIC } - - it 'rejects the snippet' do - expect { update_snippet(title: 'Foo') } - .not_to change { snippet.reload.title } - end - - it 'creates a spam log' do - expect { update_snippet(title: 'Foo') } - .to change { SpamLog.count }.by(1) - end - end - - context 'when the private snippet is made public' do - let(:visibility_level) { Snippet::PRIVATE } - - it 'rejects the snippet' do - expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) } - .not_to change { snippet.reload.title } - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']).to eq({ "error" => "Spam detected" }) - end - - it 'creates a spam log' do - expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) } - .to change { SpamLog.count }.by(1) - end - end - end - end - - describe 'DELETE /projects/:project_id/snippets/:id/' do - let(:snippet) { create(:project_snippet, author: admin) } - - it 'deletes snippet' do - admin = create(:admin) - snippet = create(:project_snippet, author: admin) - - delete v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin) - - expect(response).to have_gitlab_http_status(200) - end - - it 'returns 404 for invalid snippet id' do - delete v3_api("/projects/#{snippet.project.id}/snippets/1234", admin) - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Snippet Not Found') - end - end - - describe 'GET /projects/:project_id/snippets/:id/raw' do - let(:snippet) { create(:project_snippet, author: admin) } - - it 'returns raw text' do - get v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/raw", admin) - - expect(response).to have_gitlab_http_status(200) - expect(response.content_type).to eq 'text/plain' - expect(response.body).to eq(snippet.content) - end - - it 'returns 404 for invalid snippet id' do - delete v3_api("/projects/#{snippet.project.id}/snippets/1234", admin) - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Snippet Not Found') - end - end -end diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb deleted file mode 100644 index 158ddf171bc..00000000000 --- a/spec/requests/api/v3/projects_spec.rb +++ /dev/null @@ -1,1495 +0,0 @@ -require 'spec_helper' - -describe API::V3::Projects do - let(:user) { create(:user) } - let(:user2) { create(:user) } - let(:user3) { create(:user) } - let(:admin) { create(:admin) } - let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } - let(:project2) { create(:project, creator_id: user.id, namespace: user.namespace) } - let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') } - let(:project_member) { create(:project_member, :developer, user: user3, project: project) } - let(:user4) { create(:user) } - let(:project3) do - create(:project, - :private, - :repository, - name: 'second_project', - path: 'second_project', - creator_id: user.id, - namespace: user.namespace, - merge_requests_enabled: false, - issues_enabled: false, wiki_enabled: false, - snippets_enabled: false) - end - let(:project_member2) do - create(:project_member, - user: user4, - project: project3, - access_level: ProjectMember::MASTER) - end - let(:project4) do - create(:project, - name: 'third_project', - path: 'third_project', - creator_id: user4.id, - namespace: user4.namespace) - end - - describe 'GET /projects' do - before { project } - - context 'when unauthenticated' do - it 'returns authentication error' do - get v3_api('/projects') - expect(response).to have_gitlab_http_status(401) - end - end - - context 'when authenticated as regular user' do - it 'returns an array of projects' do - get v3_api('/projects', user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['name']).to eq(project.name) - expect(json_response.first['owner']['username']).to eq(user.username) - end - - it 'includes the project labels as the tag_list' do - get v3_api('/projects', user) - expect(response.status).to eq 200 - expect(json_response).to be_an Array - expect(json_response.first.keys).to include('tag_list') - end - - it 'includes open_issues_count' do - get v3_api('/projects', user) - expect(response.status).to eq 200 - expect(json_response).to be_an Array - expect(json_response.first.keys).to include('open_issues_count') - end - - it 'does not include open_issues_count' do - project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) - - get v3_api('/projects', user) - expect(response.status).to eq 200 - expect(json_response).to be_an Array - expect(json_response.first.keys).not_to include('open_issues_count') - end - - context 'GET /projects?simple=true' do - it 'returns a simplified version of all the projects' do - expected_keys = %w( - id description default_branch tag_list - ssh_url_to_repo http_url_to_repo web_url readme_url - name name_with_namespace - path path_with_namespace - star_count forks_count - created_at last_activity_at - avatar_url - ) - - get v3_api('/projects?simple=true', user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first.keys).to match_array expected_keys - end - end - - context 'and using search' do - it 'returns searched project' do - get v3_api('/projects', user), { search: project.name } - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - end - end - - context 'and using the visibility filter' do - it 'filters based on private visibility param' do - get v3_api('/projects', user), { visibility: 'private' } - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PRIVATE).count) - end - - it 'filters based on internal visibility param' do - get v3_api('/projects', user), { visibility: 'internal' } - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::INTERNAL).count) - end - - it 'filters based on public visibility param' do - get v3_api('/projects', user), { visibility: 'public' } - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PUBLIC).count) - end - end - - context 'and using archived' do - let!(:archived_project) { create(:project, creator_id: user.id, namespace: user.namespace, archived: true) } - - it 'returns archived project' do - get v3_api('/projects?archived=true', user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(archived_project.id) - end - - it 'returns non-archived project' do - get v3_api('/projects?archived=false', user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(project.id) - end - - it 'returns all project' do - get v3_api('/projects', user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - end - end - - context 'and using sorting' do - before do - project2 - project3 - end - - it 'returns the correct order when sorted by id' do - get v3_api('/projects', user), { order_by: 'id', sort: 'desc' } - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['id']).to eq(project3.id) - end - end - end - end - - describe 'GET /projects/all' do - before { project } - - context 'when unauthenticated' do - it 'returns authentication error' do - get v3_api('/projects/all') - expect(response).to have_gitlab_http_status(401) - end - end - - context 'when authenticated as regular user' do - it 'returns authentication error' do - get v3_api('/projects/all', user) - expect(response).to have_gitlab_http_status(403) - end - end - - context 'when authenticated as admin' do - it 'returns an array of all projects' do - get v3_api('/projects/all', admin) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - - expect(json_response).to satisfy do |response| - response.one? do |entry| - entry.key?('permissions') && - entry['name'] == project.name && - entry['owner']['username'] == user.username - end - end - end - - it "does not include statistics by default" do - get v3_api('/projects/all', admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first).not_to include('statistics') - end - - it "includes statistics if requested" do - get v3_api('/projects/all', admin), statistics: true - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first).to include 'statistics' - end - end - end - - describe 'GET /projects/owned' do - before do - project3 - project4 - end - - context 'when unauthenticated' do - it 'returns authentication error' do - get v3_api('/projects/owned') - expect(response).to have_gitlab_http_status(401) - end - end - - context 'when authenticated as project owner' do - it 'returns an array of projects the user owns' do - get v3_api('/projects/owned', user4) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['name']).to eq(project4.name) - expect(json_response.first['owner']['username']).to eq(user4.username) - end - - it "does not include statistics by default" do - get v3_api('/projects/owned', user4) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first).not_to include('statistics') - end - - it "includes statistics if requested" do - attributes = { - commit_count: 23, - storage_size: 702, - repository_size: 123, - lfs_objects_size: 234, - build_artifacts_size: 345 - } - - project4.statistics.update!(attributes) - - get v3_api('/projects/owned', user4), statistics: true - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['statistics']).to eq attributes.stringify_keys - end - end - end - - describe 'GET /projects/visible' do - shared_examples_for 'visible projects response' do - it 'returns the visible projects' do - get v3_api('/projects/visible', current_user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.map { |p| p['id'] }).to contain_exactly(*projects.map(&:id)) - end - end - - let!(:public_project) { create(:project, :public) } - before do - project - project2 - project3 - project4 - end - - context 'when unauthenticated' do - it_behaves_like 'visible projects response' do - let(:current_user) { nil } - let(:projects) { [public_project] } - end - end - - context 'when authenticated' do - it_behaves_like 'visible projects response' do - let(:current_user) { user } - let(:projects) { [public_project, project, project2, project3] } - end - end - - context 'when authenticated as a different user' do - it_behaves_like 'visible projects response' do - let(:current_user) { user2 } - let(:projects) { [public_project] } - end - end - end - - describe 'GET /projects/starred' do - let(:public_project) { create(:project, :public) } - - before do - project_member - user3.update_attributes(starred_projects: [project, project2, project3, public_project]) - end - - it 'returns the starred projects viewable by the user' do - get v3_api('/projects/starred', user3) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.map { |project| project['id'] }).to contain_exactly(project.id, public_project.id) - end - end - - describe 'POST /projects' do - context 'maximum number of projects reached' do - it 'does not create new project and respond with 403' do - allow_any_instance_of(User).to receive(:projects_limit_left).and_return(0) - expect { post v3_api('/projects', user2), name: 'foo' } - .to change {Project.count}.by(0) - expect(response).to have_gitlab_http_status(403) - end - end - - it 'creates new project without path but with name and returns 201' do - expect { post v3_api('/projects', user), name: 'Foo Project' } - .to change { Project.count }.by(1) - expect(response).to have_gitlab_http_status(201) - - project = Project.first - - expect(project.name).to eq('Foo Project') - expect(project.path).to eq('foo-project') - end - - it 'creates new project without name but with path and returns 201' do - expect { post v3_api('/projects', user), path: 'foo_project' } - .to change { Project.count }.by(1) - expect(response).to have_gitlab_http_status(201) - - project = Project.first - - expect(project.name).to eq('foo_project') - expect(project.path).to eq('foo_project') - end - - it 'creates new project name and path and returns 201' do - expect { post v3_api('/projects', user), path: 'foo-Project', name: 'Foo Project' } - .to change { Project.count }.by(1) - expect(response).to have_gitlab_http_status(201) - - project = Project.first - - expect(project.name).to eq('Foo Project') - expect(project.path).to eq('foo-Project') - end - - it 'creates last project before reaching project limit' do - allow_any_instance_of(User).to receive(:projects_limit_left).and_return(1) - post v3_api('/projects', user2), name: 'foo' - expect(response).to have_gitlab_http_status(201) - end - - it 'does not create new project without name or path and return 400' do - expect { post v3_api('/projects', user) }.not_to change { Project.count } - expect(response).to have_gitlab_http_status(400) - end - - it "assigns attributes to project" do - project = attributes_for(:project, { - path: 'camelCasePath', - issues_enabled: false, - merge_requests_enabled: false, - wiki_enabled: false, - only_allow_merge_if_build_succeeds: false, - request_access_enabled: true, - only_allow_merge_if_all_discussions_are_resolved: false - }) - - post v3_api('/projects', user), project - - project.each_pair do |k, v| - next if %i[storage_version has_external_issue_tracker issues_enabled merge_requests_enabled wiki_enabled].include?(k) - - expect(json_response[k.to_s]).to eq(v) - end - - # Check feature permissions attributes - project = Project.find_by_path(project[:path]) - expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED) - expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::DISABLED) - expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::DISABLED) - end - - it 'sets a project as public' do - project = attributes_for(:project, :public) - post v3_api('/projects', user), project - expect(json_response['public']).to be_truthy - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) - end - - it 'sets a project as public using :public' do - project = attributes_for(:project, { public: true }) - post v3_api('/projects', user), project - expect(json_response['public']).to be_truthy - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) - end - - it 'sets a project as internal' do - project = attributes_for(:project, :internal) - post v3_api('/projects', user), project - expect(json_response['public']).to be_falsey - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) - end - - it 'sets a project as internal overriding :public' do - project = attributes_for(:project, :internal, { public: true }) - post v3_api('/projects', user), project - expect(json_response['public']).to be_falsey - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) - end - - it 'sets a project as private' do - project = attributes_for(:project, :private) - post v3_api('/projects', user), project - expect(json_response['public']).to be_falsey - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) - end - - it 'sets a project as private using :public' do - project = attributes_for(:project, { public: false }) - post v3_api('/projects', user), project - expect(json_response['public']).to be_falsey - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) - end - - it 'sets a project as allowing merge even if build fails' do - project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false }) - post v3_api('/projects', user), project - expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey - end - - it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do - project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true }) - post v3_api('/projects', user), project - expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy - end - - it 'sets a project as allowing merge even if discussions are unresolved' do - project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false }) - - post v3_api('/projects', user), project - - expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey - end - - it 'sets a project as allowing merge if only_allow_merge_if_all_discussions_are_resolved is nil' do - project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: nil) - - post v3_api('/projects', user), project - - expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey - end - - it 'sets a project as allowing merge only if all discussions are resolved' do - project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true }) - - post v3_api('/projects', user), project - - expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy - end - - context 'when a visibility level is restricted' do - before do - @project = attributes_for(:project, { public: true }) - stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) - end - - it 'does not allow a non-admin to use a restricted visibility level' do - post v3_api('/projects', user), @project - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']['visibility_level'].first).to( - match('restricted by your GitLab administrator') - ) - end - - it 'allows an admin to override restricted visibility settings' do - post v3_api('/projects', admin), @project - expect(json_response['public']).to be_truthy - expect(json_response['visibility_level']).to( - eq(Gitlab::VisibilityLevel::PUBLIC) - ) - end - end - end - - describe 'POST /projects/user/:id' do - before { project } - before { admin } - - it 'should create new project without path and return 201' do - expect { post v3_api("/projects/user/#{user.id}", admin), name: 'foo' }.to change {Project.count}.by(1) - expect(response).to have_gitlab_http_status(201) - end - - it 'responds with 400 on failure and not project' do - expect { post v3_api("/projects/user/#{user.id}", admin) } - .not_to change { Project.count } - - expect(response).to have_gitlab_http_status(400) - expect(json_response['error']).to eq('name is missing') - end - - it 'assigns attributes to project' do - project = attributes_for(:project, { - issues_enabled: false, - merge_requests_enabled: false, - wiki_enabled: false, - request_access_enabled: true - }) - - post v3_api("/projects/user/#{user.id}", admin), project - - expect(response).to have_gitlab_http_status(201) - project.each_pair do |k, v| - next if %i[storage_version has_external_issue_tracker path].include?(k) - - expect(json_response[k.to_s]).to eq(v) - end - end - - it 'sets a project as public' do - project = attributes_for(:project, :public) - post v3_api("/projects/user/#{user.id}", admin), project - - expect(response).to have_gitlab_http_status(201) - expect(json_response['public']).to be_truthy - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) - end - - it 'sets a project as public using :public' do - project = attributes_for(:project, { public: true }) - post v3_api("/projects/user/#{user.id}", admin), project - - expect(response).to have_gitlab_http_status(201) - expect(json_response['public']).to be_truthy - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) - end - - it 'sets a project as internal' do - project = attributes_for(:project, :internal) - post v3_api("/projects/user/#{user.id}", admin), project - - expect(response).to have_gitlab_http_status(201) - expect(json_response['public']).to be_falsey - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) - end - - it 'sets a project as internal overriding :public' do - project = attributes_for(:project, :internal, { public: true }) - post v3_api("/projects/user/#{user.id}", admin), project - expect(response).to have_gitlab_http_status(201) - expect(json_response['public']).to be_falsey - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) - end - - it 'sets a project as private' do - project = attributes_for(:project, :private) - post v3_api("/projects/user/#{user.id}", admin), project - expect(json_response['public']).to be_falsey - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) - end - - it 'sets a project as private using :public' do - project = attributes_for(:project, { public: false }) - post v3_api("/projects/user/#{user.id}", admin), project - expect(json_response['public']).to be_falsey - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) - end - - it 'sets a project as allowing merge even if build fails' do - project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false }) - post v3_api("/projects/user/#{user.id}", admin), project - expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey - end - - it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do - project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true }) - post v3_api("/projects/user/#{user.id}", admin), project - expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy - end - - it 'sets a project as allowing merge even if discussions are unresolved' do - project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false }) - - post v3_api("/projects/user/#{user.id}", admin), project - - expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey - end - - it 'sets a project as allowing merge only if all discussions are resolved' do - project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true }) - - post v3_api("/projects/user/#{user.id}", admin), project - - expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy - end - end - - describe "POST /projects/:id/uploads" do - before { project } - - it "uploads the file and returns its info" do - post v3_api("/projects/#{project.id}/uploads", user), file: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") - - expect(response).to have_gitlab_http_status(201) - expect(json_response['alt']).to eq("dk") - expect(json_response['url']).to start_with("/uploads/") - expect(json_response['url']).to end_with("/dk.png") - end - end - - describe 'GET /projects/:id' do - context 'when unauthenticated' do - it 'returns the public projects' do - public_project = create(:project, :public) - - get v3_api("/projects/#{public_project.id}") - - expect(response).to have_gitlab_http_status(200) - expect(json_response['id']).to eq(public_project.id) - expect(json_response['description']).to eq(public_project.description) - expect(json_response['default_branch']).to eq(public_project.default_branch) - expect(json_response.keys).not_to include('permissions') - end - end - - context 'when authenticated' do - before do - project - end - - it 'returns a project by id' do - group = create(:group) - link = create(:project_group_link, project: project, group: group) - - get v3_api("/projects/#{project.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['id']).to eq(project.id) - expect(json_response['description']).to eq(project.description) - expect(json_response['default_branch']).to eq(project.default_branch) - expect(json_response['tag_list']).to be_an Array - expect(json_response['public']).to be_falsey - expect(json_response['archived']).to be_falsey - expect(json_response['visibility_level']).to be_present - expect(json_response['ssh_url_to_repo']).to be_present - expect(json_response['http_url_to_repo']).to be_present - expect(json_response['web_url']).to be_present - expect(json_response['owner']).to be_a Hash - expect(json_response['owner']).to be_a Hash - expect(json_response['name']).to eq(project.name) - expect(json_response['path']).to be_present - expect(json_response['issues_enabled']).to be_present - expect(json_response['merge_requests_enabled']).to be_present - expect(json_response['wiki_enabled']).to be_present - expect(json_response['builds_enabled']).to be_present - expect(json_response['snippets_enabled']).to be_present - expect(json_response['resolve_outdated_diff_discussions']).to eq(project.resolve_outdated_diff_discussions) - expect(json_response['container_registry_enabled']).to be_present - expect(json_response['created_at']).to be_present - expect(json_response['last_activity_at']).to be_present - expect(json_response['shared_runners_enabled']).to be_present - expect(json_response['creator_id']).to be_present - expect(json_response['namespace']).to be_present - expect(json_response['avatar_url']).to be_nil - expect(json_response['star_count']).to be_present - expect(json_response['forks_count']).to be_present - expect(json_response['public_builds']).to be_present - expect(json_response['shared_with_groups']).to be_an Array - expect(json_response['shared_with_groups'].length).to eq(1) - expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id) - expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name) - expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access) - expect(json_response['only_allow_merge_if_build_succeeds']).to eq(project.only_allow_merge_if_pipeline_succeeds) - expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved) - end - - it 'returns a project by path name' do - get v3_api("/projects/#{project.id}", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response['name']).to eq(project.name) - end - - it 'returns a 404 error if not found' do - get v3_api('/projects/42', user) - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Project Not Found') - end - - it 'returns a 404 error if user is not a member' do - other_user = create(:user) - get v3_api("/projects/#{project.id}", other_user) - expect(response).to have_gitlab_http_status(404) - end - - it 'handles users with dots' do - dot_user = create(:user, username: 'dot.user') - project = create(:project, creator_id: dot_user.id, namespace: dot_user.namespace) - - get v3_api("/projects/#{CGI.escape(project.full_path)}", dot_user) - expect(response).to have_gitlab_http_status(200) - expect(json_response['name']).to eq(project.name) - end - - it 'exposes namespace fields' do - get v3_api("/projects/#{project.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['namespace']).to eq({ - 'id' => user.namespace.id, - 'name' => user.namespace.name, - 'path' => user.namespace.path, - 'kind' => user.namespace.kind, - 'full_path' => user.namespace.full_path, - 'parent_id' => nil - }) - end - - describe 'permissions' do - context 'all projects' do - before { project.add_master(user) } - - it 'contains permission information' do - get v3_api("/projects", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response.first['permissions']['project_access']['access_level']) - .to eq(Gitlab::Access::MASTER) - expect(json_response.first['permissions']['group_access']).to be_nil - end - end - - context 'personal project' do - it 'sets project access and returns 200' do - project.add_master(user) - get v3_api("/projects/#{project.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['permissions']['project_access']['access_level']) - .to eq(Gitlab::Access::MASTER) - expect(json_response['permissions']['group_access']).to be_nil - end - end - - context 'group project' do - let(:project2) { create(:project, group: create(:group)) } - - before { project2.group.add_owner(user) } - - it 'sets the owner and return 200' do - get v3_api("/projects/#{project2.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['permissions']['project_access']).to be_nil - expect(json_response['permissions']['group_access']['access_level']) - .to eq(Gitlab::Access::OWNER) - end - end - end - end - end - - describe 'GET /projects/:id/events' do - shared_examples_for 'project events response' do - it 'returns the project events' do - member = create(:user) - create(:project_member, :developer, user: member, project: project) - note = create(:note_on_issue, note: 'What an awesome day!', project: project) - EventCreateService.new.leave_note(note, note.author) - - get v3_api("/projects/#{project.id}/events", current_user) - - expect(response).to have_gitlab_http_status(200) - - first_event = json_response.first - - expect(first_event['action_name']).to eq('commented on') - expect(first_event['note']['body']).to eq('What an awesome day!') - - last_event = json_response.last - - expect(last_event['action_name']).to eq('joined') - expect(last_event['project_id'].to_i).to eq(project.id) - expect(last_event['author_username']).to eq(member.username) - expect(last_event['author']['name']).to eq(member.name) - end - end - - context 'when unauthenticated' do - it_behaves_like 'project events response' do - let(:project) { create(:project, :public) } - let(:current_user) { nil } - end - end - - context 'when authenticated' do - context 'valid request' do - it_behaves_like 'project events response' do - let(:current_user) { user } - end - end - - it 'returns a 404 error if not found' do - get v3_api('/projects/42/events', user) - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Project Not Found') - end - - it 'returns a 404 error if user is not a member' do - other_user = create(:user) - - get v3_api("/projects/#{project.id}/events", other_user) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe 'GET /projects/:id/users' do - shared_examples_for 'project users response' do - it 'returns the project users' do - member = project.owner - - get v3_api("/projects/#{project.id}/users", current_user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to eq(1) - - first_user = json_response.first - - expect(first_user['username']).to eq(member.username) - expect(first_user['name']).to eq(member.name) - expect(first_user.keys).to contain_exactly(*%w[name username id state avatar_url web_url]) - end - end - - context 'when unauthenticated' do - it_behaves_like 'project users response' do - let(:project) { create(:project, :public) } - let(:current_user) { nil } - end - end - - context 'when authenticated' do - context 'valid request' do - it_behaves_like 'project users response' do - let(:current_user) { user } - end - end - - it 'returns a 404 error if not found' do - get v3_api('/projects/42/users', user) - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Project Not Found') - end - - it 'returns a 404 error if user is not a member' do - other_user = create(:user) - - get v3_api("/projects/#{project.id}/users", other_user) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe 'GET /projects/:id/snippets' do - before { snippet } - - it 'returns an array of project snippets' do - get v3_api("/projects/#{project.id}/snippets", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['title']).to eq(snippet.title) - end - end - - describe 'GET /projects/:id/snippets/:snippet_id' do - it 'returns a project snippet' do - get v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq(snippet.title) - end - - it 'returns a 404 error if snippet id not found' do - get v3_api("/projects/#{project.id}/snippets/1234", user) - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'POST /projects/:id/snippets' do - it 'creates a new project snippet' do - post v3_api("/projects/#{project.id}/snippets", user), - title: 'v3_api test', file_name: 'sample.rb', code: 'test', - visibility_level: '0' - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('v3_api test') - end - - it 'returns a 400 error if invalid snippet is given' do - post v3_api("/projects/#{project.id}/snippets", user) - expect(status).to eq(400) - end - end - - describe 'PUT /projects/:id/snippets/:snippet_id' do - it 'updates an existing project snippet' do - put v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user), - code: 'updated code' - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq('example') - expect(snippet.reload.content).to eq('updated code') - end - - it 'updates an existing project snippet with new title' do - put v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user), - title: 'other v3_api test' - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq('other v3_api test') - end - end - - describe 'DELETE /projects/:id/snippets/:snippet_id' do - before { snippet } - - it 'deletes existing project snippet' do - expect do - delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user) - end.to change { Snippet.count }.by(-1) - expect(response).to have_gitlab_http_status(200) - end - - it 'returns 404 when deleting unknown snippet id' do - delete v3_api("/projects/#{project.id}/snippets/1234", user) - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'GET /projects/:id/snippets/:snippet_id/raw' do - it 'gets a raw project snippet' do - get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/raw", user) - expect(response).to have_gitlab_http_status(200) - end - - it 'returns a 404 error if raw project snippet not found' do - get v3_api("/projects/#{project.id}/snippets/5555/raw", user) - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'fork management' do - let(:project_fork_target) { create(:project) } - let(:project_fork_source) { create(:project, :public) } - - describe 'POST /projects/:id/fork/:forked_from_id' do - let(:new_project_fork_source) { create(:project, :public) } - - it "is not available for non admin users" do - post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", user) - expect(response).to have_gitlab_http_status(403) - end - - it 'allows project to be forked from an existing project' do - expect(project_fork_target.forked?).not_to be_truthy - post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) - expect(response).to have_gitlab_http_status(201) - project_fork_target.reload - expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id) - expect(project_fork_target.forked_project_link).not_to be_nil - expect(project_fork_target.forked?).to be_truthy - end - - it 'refreshes the forks count cachce' do - expect(project_fork_source.forks_count).to be_zero - - post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) - - expect(project_fork_source.forks_count).to eq(1) - end - - it 'fails if forked_from project which does not exist' do - post v3_api("/projects/#{project_fork_target.id}/fork/9999", admin) - expect(response).to have_gitlab_http_status(404) - end - - it 'fails with 409 if already forked' do - post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) - project_fork_target.reload - expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id) - post v3_api("/projects/#{project_fork_target.id}/fork/#{new_project_fork_source.id}", admin) - expect(response).to have_gitlab_http_status(409) - project_fork_target.reload - expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id) - expect(project_fork_target.forked?).to be_truthy - end - end - - describe 'DELETE /projects/:id/fork' do - it "is not visible to users outside group" do - delete v3_api("/projects/#{project_fork_target.id}/fork", user) - expect(response).to have_gitlab_http_status(404) - end - - context 'when users belong to project group' do - let(:project_fork_target) { create(:project, group: create(:group)) } - - before do - project_fork_target.group.add_owner user - project_fork_target.group.add_developer user2 - end - - it 'is forbidden to non-owner users' do - delete v3_api("/projects/#{project_fork_target.id}/fork", user2) - expect(response).to have_gitlab_http_status(403) - end - - it 'makes forked project unforked' do - post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) - project_fork_target.reload - expect(project_fork_target.forked_from_project).not_to be_nil - expect(project_fork_target.forked?).to be_truthy - delete v3_api("/projects/#{project_fork_target.id}/fork", admin) - expect(response).to have_gitlab_http_status(200) - project_fork_target.reload - expect(project_fork_target.forked_from_project).to be_nil - expect(project_fork_target.forked?).not_to be_truthy - end - - it 'is idempotent if not forked' do - expect(project_fork_target.forked_from_project).to be_nil - delete v3_api("/projects/#{project_fork_target.id}/fork", admin) - expect(response).to have_gitlab_http_status(304) - expect(project_fork_target.reload.forked_from_project).to be_nil - end - end - end - end - - describe "POST /projects/:id/share" do - let(:group) { create(:group) } - - it "shares project with group" do - expires_at = 10.days.from_now.to_date - - expect do - post v3_api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER, expires_at: expires_at - end.to change { ProjectGroupLink.count }.by(1) - - expect(response).to have_gitlab_http_status(201) - expect(json_response['group_id']).to eq(group.id) - expect(json_response['group_access']).to eq(Gitlab::Access::DEVELOPER) - expect(json_response['expires_at']).to eq(expires_at.to_s) - end - - it "returns a 400 error when group id is not given" do - post v3_api("/projects/#{project.id}/share", user), group_access: Gitlab::Access::DEVELOPER - expect(response).to have_gitlab_http_status(400) - end - - it "returns a 400 error when access level is not given" do - post v3_api("/projects/#{project.id}/share", user), group_id: group.id - expect(response).to have_gitlab_http_status(400) - end - - it "returns a 400 error when sharing is disabled" do - project.namespace.update(share_with_group_lock: true) - post v3_api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER - expect(response).to have_gitlab_http_status(400) - end - - it 'returns a 404 error when user cannot read group' do - private_group = create(:group, :private) - - post v3_api("/projects/#{project.id}/share", user), group_id: private_group.id, group_access: Gitlab::Access::DEVELOPER - - expect(response).to have_gitlab_http_status(404) - end - - it 'returns a 404 error when group does not exist' do - post v3_api("/projects/#{project.id}/share", user), group_id: 1234, group_access: Gitlab::Access::DEVELOPER - - expect(response).to have_gitlab_http_status(404) - end - - it "returns a 400 error when wrong params passed" do - post v3_api("/projects/#{project.id}/share", user), group_id: group.id, group_access: 1234 - - expect(response).to have_gitlab_http_status(400) - expect(json_response['error']).to eq 'group_access does not have a valid value' - end - end - - describe 'DELETE /projects/:id/share/:group_id' do - it 'returns 204 when deleting a group share' do - group = create(:group, :public) - create(:project_group_link, group: group, project: project) - - delete v3_api("/projects/#{project.id}/share/#{group.id}", user) - - expect(response).to have_gitlab_http_status(204) - expect(project.project_group_links).to be_empty - end - - it 'returns a 400 when group id is not an integer' do - delete v3_api("/projects/#{project.id}/share/foo", user) - - expect(response).to have_gitlab_http_status(400) - end - - it 'returns a 404 error when group link does not exist' do - delete v3_api("/projects/#{project.id}/share/1234", user) - - expect(response).to have_gitlab_http_status(404) - end - - it 'returns a 404 error when project does not exist' do - delete v3_api("/projects/123/share/1234", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'GET /projects/search/:query' do - let!(:query) { 'query'} - let!(:search) { create(:project, name: query, creator_id: user.id, namespace: user.namespace) } - let!(:pre) { create(:project, name: "pre_#{query}", creator_id: user.id, namespace: user.namespace) } - let!(:post) { create(:project, name: "#{query}_post", creator_id: user.id, namespace: user.namespace) } - let!(:pre_post) { create(:project, name: "pre_#{query}_post", creator_id: user.id, namespace: user.namespace) } - let!(:unfound) { create(:project, name: 'unfound', creator_id: user.id, namespace: user.namespace) } - let!(:internal) { create(:project, :internal, name: "internal #{query}") } - let!(:unfound_internal) { create(:project, :internal, name: 'unfound internal') } - let!(:public) { create(:project, :public, name: "public #{query}") } - let!(:unfound_public) { create(:project, :public, name: 'unfound public') } - let!(:one_dot_two) { create(:project, :public, name: "one.dot.two") } - - shared_examples_for 'project search response' do |args = {}| - it 'returns project search responses' do - get v3_api("/projects/search/#{args[:query]}", current_user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to eq(args[:results]) - json_response.each { |project| expect(project['name']).to match(args[:match_regex] || /.*#{args[:query]}.*/) } - end - end - - context 'when unauthenticated' do - it_behaves_like 'project search response', query: 'query', results: 1 do - let(:current_user) { nil } - end - end - - context 'when authenticated' do - it_behaves_like 'project search response', query: 'query', results: 6 do - let(:current_user) { user } - end - it_behaves_like 'project search response', query: 'one.dot.two', results: 1 do - let(:current_user) { user } - end - end - - context 'when authenticated as a different user' do - it_behaves_like 'project search response', query: 'query', results: 2, match_regex: /(internal|public) query/ do - let(:current_user) { user2 } - end - end - end - - describe 'PUT /projects/:id' do - before { project } - before { user } - before { user3 } - before { user4 } - before { project3 } - before { project4 } - before { project_member2 } - before { project_member } - - context 'when unauthenticated' do - it 'returns authentication error' do - project_param = { name: 'bar' } - put v3_api("/projects/#{project.id}"), project_param - expect(response).to have_gitlab_http_status(401) - end - end - - context 'when authenticated as project owner' do - it 'updates name' do - project_param = { name: 'bar' } - put v3_api("/projects/#{project.id}", user), project_param - expect(response).to have_gitlab_http_status(200) - project_param.each_pair do |k, v| - expect(json_response[k.to_s]).to eq(v) - end - end - - it 'updates visibility_level' do - project_param = { visibility_level: 20 } - put v3_api("/projects/#{project3.id}", user), project_param - expect(response).to have_gitlab_http_status(200) - project_param.each_pair do |k, v| - expect(json_response[k.to_s]).to eq(v) - end - end - - it 'updates visibility_level from public to private' do - project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC }) - project_param = { public: false } - put v3_api("/projects/#{project3.id}", user), project_param - expect(response).to have_gitlab_http_status(200) - project_param.each_pair do |k, v| - expect(json_response[k.to_s]).to eq(v) - end - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) - end - - it 'does not update name to existing name' do - project_param = { name: project3.name } - put v3_api("/projects/#{project.id}", user), project_param - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']['name']).to eq(['has already been taken']) - end - - it 'updates request_access_enabled' do - project_param = { request_access_enabled: false } - - put v3_api("/projects/#{project.id}", user), project_param - - expect(response).to have_gitlab_http_status(200) - expect(json_response['request_access_enabled']).to eq(false) - end - - it 'updates path & name to existing path & name in different namespace' do - project_param = { path: project4.path, name: project4.name } - put v3_api("/projects/#{project3.id}", user), project_param - expect(response).to have_gitlab_http_status(200) - project_param.each_pair do |k, v| - expect(json_response[k.to_s]).to eq(v) - end - end - end - - context 'when authenticated as project master' do - it 'updates path' do - project_param = { path: 'bar' } - put v3_api("/projects/#{project3.id}", user4), project_param - expect(response).to have_gitlab_http_status(200) - project_param.each_pair do |k, v| - expect(json_response[k.to_s]).to eq(v) - end - end - - it 'updates other attributes' do - project_param = { issues_enabled: true, - wiki_enabled: true, - snippets_enabled: true, - merge_requests_enabled: true, - description: 'new description' } - - put v3_api("/projects/#{project3.id}", user4), project_param - expect(response).to have_gitlab_http_status(200) - project_param.each_pair do |k, v| - expect(json_response[k.to_s]).to eq(v) - end - end - - it 'does not update path to existing path' do - project_param = { path: project.path } - put v3_api("/projects/#{project3.id}", user4), project_param - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']['path']).to eq(['has already been taken']) - end - - it 'does not update name' do - project_param = { name: 'bar' } - put v3_api("/projects/#{project3.id}", user4), project_param - expect(response).to have_gitlab_http_status(403) - end - - it 'does not update visibility_level' do - project_param = { visibility_level: 20 } - put v3_api("/projects/#{project3.id}", user4), project_param - expect(response).to have_gitlab_http_status(403) - end - end - - context 'when authenticated as project developer' do - it 'does not update other attributes' do - project_param = { path: 'bar', - issues_enabled: true, - wiki_enabled: true, - snippets_enabled: true, - merge_requests_enabled: true, - description: 'new description', - request_access_enabled: true } - put v3_api("/projects/#{project.id}", user3), project_param - expect(response).to have_gitlab_http_status(403) - end - end - end - - describe 'POST /projects/:id/archive' do - context 'on an unarchived project' do - it 'archives the project' do - post v3_api("/projects/#{project.id}/archive", user) - - expect(response).to have_gitlab_http_status(201) - expect(json_response['archived']).to be_truthy - end - end - - context 'on an archived project' do - before do - project.archive! - end - - it 'remains archived' do - post v3_api("/projects/#{project.id}/archive", user) - - expect(response).to have_gitlab_http_status(201) - expect(json_response['archived']).to be_truthy - end - end - - context 'user without archiving rights to the project' do - before do - project.add_developer(user3) - end - - it 'rejects the action' do - post v3_api("/projects/#{project.id}/archive", user3) - - expect(response).to have_gitlab_http_status(403) - end - end - end - - describe 'POST /projects/:id/unarchive' do - context 'on an unarchived project' do - it 'remains unarchived' do - post v3_api("/projects/#{project.id}/unarchive", user) - - expect(response).to have_gitlab_http_status(201) - expect(json_response['archived']).to be_falsey - end - end - - context 'on an archived project' do - before do - project.archive! - end - - it 'unarchives the project' do - post v3_api("/projects/#{project.id}/unarchive", user) - - expect(response).to have_gitlab_http_status(201) - expect(json_response['archived']).to be_falsey - end - end - - context 'user without archiving rights to the project' do - before do - project.add_developer(user3) - end - - it 'rejects the action' do - post v3_api("/projects/#{project.id}/unarchive", user3) - - expect(response).to have_gitlab_http_status(403) - end - end - end - - describe 'POST /projects/:id/star' do - context 'on an unstarred project' do - it 'stars the project' do - expect { post v3_api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(1) - - expect(response).to have_gitlab_http_status(201) - expect(json_response['star_count']).to eq(1) - end - end - - context 'on a starred project' do - before do - user.toggle_star(project) - project.reload - end - - it 'does not modify the star count' do - expect { post v3_api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count } - - expect(response).to have_gitlab_http_status(304) - end - end - end - - describe 'DELETE /projects/:id/star' do - context 'on a starred project' do - before do - user.toggle_star(project) - project.reload - end - - it 'unstars the project' do - expect { delete v3_api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(-1) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['star_count']).to eq(0) - end - end - - context 'on an unstarred project' do - it 'does not modify the star count' do - expect { delete v3_api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count } - - expect(response).to have_gitlab_http_status(304) - end - end - end - - describe 'DELETE /projects/:id' do - context 'when authenticated as user' do - it 'removes project' do - delete v3_api("/projects/#{project.id}", user) - expect(response).to have_gitlab_http_status(200) - end - - it 'does not remove a project if not an owner' do - user3 = create(:user) - project.add_developer(user3) - delete v3_api("/projects/#{project.id}", user3) - expect(response).to have_gitlab_http_status(403) - end - - it 'does not remove a non existing project' do - delete v3_api('/projects/1328', user) - expect(response).to have_gitlab_http_status(404) - end - - it 'does not remove a project not attached to user' do - delete v3_api("/projects/#{project.id}", user2) - expect(response).to have_gitlab_http_status(404) - end - end - - context 'when authenticated as admin' do - it 'removes any existing project' do - delete v3_api("/projects/#{project.id}", admin) - expect(response).to have_gitlab_http_status(200) - end - - it 'does not remove a non existing project' do - delete v3_api('/projects/1328', admin) - expect(response).to have_gitlab_http_status(404) - end - end - end -end diff --git a/spec/requests/api/v3/repositories_spec.rb b/spec/requests/api/v3/repositories_spec.rb deleted file mode 100644 index 0167eb2c4f6..00000000000 --- a/spec/requests/api/v3/repositories_spec.rb +++ /dev/null @@ -1,366 +0,0 @@ -require 'spec_helper' -require 'mime/types' - -describe API::V3::Repositories do - include RepoHelpers - include WorkhorseHelpers - - let(:user) { create(:user) } - let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } } - let!(:project) { create(:project, :repository, creator: user) } - let!(:master) { create(:project_member, :master, user: user, project: project) } - - describe "GET /projects/:id/repository/tree" do - let(:route) { "/projects/#{project.id}/repository/tree" } - - shared_examples_for 'repository tree' do - it 'returns the repository tree' do - get v3_api(route, current_user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - - first_commit = json_response.first - expect(first_commit['name']).to eq('bar') - expect(first_commit['type']).to eq('tree') - expect(first_commit['mode']).to eq('040000') - end - - context 'when ref does not exist' do - it_behaves_like '404 response' do - let(:request) { get v3_api("#{route}?ref_name=foo", current_user) } - let(:message) { '404 Tree Not Found' } - end - end - - context 'when repository is disabled' do - include_context 'disabled repository' - - it_behaves_like '403 response' do - let(:request) { get v3_api(route, current_user) } - end - end - - context 'with recursive=1' do - it 'returns recursive project paths tree' do - get v3_api("#{route}?recursive=1", current_user) - - expect(response.status).to eq(200) - expect(json_response).to be_an Array - expect(json_response[4]['name']).to eq('html') - expect(json_response[4]['path']).to eq('files/html') - expect(json_response[4]['type']).to eq('tree') - expect(json_response[4]['mode']).to eq('040000') - end - - context 'when repository is disabled' do - include_context 'disabled repository' - - it_behaves_like '403 response' do - let(:request) { get v3_api(route, current_user) } - end - end - - context 'when ref does not exist' do - it_behaves_like '404 response' do - let(:request) { get v3_api("#{route}?recursive=1&ref_name=foo", current_user) } - let(:message) { '404 Tree Not Found' } - end - end - end - end - - context 'when unauthenticated', 'and project is public' do - it_behaves_like 'repository tree' do - let(:project) { create(:project, :public, :repository) } - let(:current_user) { nil } - end - end - - context 'when unauthenticated', 'and project is private' do - it_behaves_like '404 response' do - let(:request) { get v3_api(route) } - let(:message) { '404 Project Not Found' } - end - end - - context 'when authenticated', 'as a developer' do - it_behaves_like 'repository tree' do - let(:current_user) { user } - end - end - - context 'when authenticated', 'as a guest' do - it_behaves_like '403 response' do - let(:request) { get v3_api(route, guest) } - end - end - end - - [ - ['blobs/:sha', 'blobs/master'], - ['blobs/:sha', 'blobs/v1.1.0'], - ['commits/:sha/blob', 'commits/master/blob'] - ].each do |desc_path, example_path| - describe "GET /projects/:id/repository/#{desc_path}" do - let(:route) { "/projects/#{project.id}/repository/#{example_path}?filepath=README.md" } - shared_examples_for 'repository blob' do - it 'returns the repository blob' do - get v3_api(route, current_user) - expect(response).to have_gitlab_http_status(200) - end - context 'when sha does not exist' do - it_behaves_like '404 response' do - let(:request) { get v3_api("/projects/#{project.id}/repository/#{desc_path.sub(':sha', 'invalid_branch_name')}?filepath=README.md", current_user) } - let(:message) { '404 Commit Not Found' } - end - end - context 'when filepath does not exist' do - it_behaves_like '404 response' do - let(:request) { get v3_api(route.sub('README.md', 'README.invalid'), current_user) } - let(:message) { '404 File Not Found' } - end - end - context 'when no filepath is given' do - it_behaves_like '400 response' do - let(:request) { get v3_api(route.sub('?filepath=README.md', ''), current_user) } - end - end - context 'when repository is disabled' do - include_context 'disabled repository' - it_behaves_like '403 response' do - let(:request) { get v3_api(route, current_user) } - end - end - end - context 'when unauthenticated', 'and project is public' do - it_behaves_like 'repository blob' do - let(:project) { create(:project, :public, :repository) } - let(:current_user) { nil } - end - end - context 'when unauthenticated', 'and project is private' do - it_behaves_like '404 response' do - let(:request) { get v3_api(route) } - let(:message) { '404 Project Not Found' } - end - end - context 'when authenticated', 'as a developer' do - it_behaves_like 'repository blob' do - let(:current_user) { user } - end - end - context 'when authenticated', 'as a guest' do - it_behaves_like '403 response' do - let(:request) { get v3_api(route, guest) } - end - end - end - end - describe "GET /projects/:id/repository/raw_blobs/:sha" do - let(:route) { "/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}" } - shared_examples_for 'repository raw blob' do - it 'returns the repository raw blob' do - get v3_api(route, current_user) - expect(response).to have_gitlab_http_status(200) - end - context 'when sha does not exist' do - it_behaves_like '404 response' do - let(:request) { get v3_api(route.sub(sample_blob.oid, '123456'), current_user) } - let(:message) { '404 Blob Not Found' } - end - end - context 'when repository is disabled' do - include_context 'disabled repository' - it_behaves_like '403 response' do - let(:request) { get v3_api(route, current_user) } - end - end - end - context 'when unauthenticated', 'and project is public' do - it_behaves_like 'repository raw blob' do - let(:project) { create(:project, :public, :repository) } - let(:current_user) { nil } - end - end - context 'when unauthenticated', 'and project is private' do - it_behaves_like '404 response' do - let(:request) { get v3_api(route) } - let(:message) { '404 Project Not Found' } - end - end - context 'when authenticated', 'as a developer' do - it_behaves_like 'repository raw blob' do - let(:current_user) { user } - end - end - context 'when authenticated', 'as a guest' do - it_behaves_like '403 response' do - let(:request) { get v3_api(route, guest) } - end - end - end - describe "GET /projects/:id/repository/archive(.:format)?:sha" do - let(:route) { "/projects/#{project.id}/repository/archive" } - shared_examples_for 'repository archive' do - it 'returns the repository archive' do - get v3_api(route, current_user) - expect(response).to have_gitlab_http_status(200) - repo_name = project.repository.name.gsub("\.git", "") - type, params = workhorse_send_data - expect(type).to eq('git-archive') - expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/) - end - it 'returns the repository archive archive.zip' do - get v3_api("/projects/#{project.id}/repository/archive.zip", user) - expect(response).to have_gitlab_http_status(200) - repo_name = project.repository.name.gsub("\.git", "") - type, params = workhorse_send_data - expect(type).to eq('git-archive') - expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/) - end - it 'returns the repository archive archive.tar.bz2' do - get v3_api("/projects/#{project.id}/repository/archive.tar.bz2", user) - expect(response).to have_gitlab_http_status(200) - repo_name = project.repository.name.gsub("\.git", "") - type, params = workhorse_send_data - expect(type).to eq('git-archive') - expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/) - end - context 'when sha does not exist' do - it_behaves_like '404 response' do - let(:request) { get v3_api("#{route}?sha=xxx", current_user) } - let(:message) { '404 File Not Found' } - end - end - end - context 'when unauthenticated', 'and project is public' do - it_behaves_like 'repository archive' do - let(:project) { create(:project, :public, :repository) } - let(:current_user) { nil } - end - end - context 'when unauthenticated', 'and project is private' do - it_behaves_like '404 response' do - let(:request) { get v3_api(route) } - let(:message) { '404 Project Not Found' } - end - end - context 'when authenticated', 'as a developer' do - it_behaves_like 'repository archive' do - let(:current_user) { user } - end - end - context 'when authenticated', 'as a guest' do - it_behaves_like '403 response' do - let(:request) { get v3_api(route, guest) } - end - end - end - - describe 'GET /projects/:id/repository/compare' do - let(:route) { "/projects/#{project.id}/repository/compare" } - shared_examples_for 'repository compare' do - it "compares branches" do - get v3_api(route, current_user), from: 'master', to: 'feature' - expect(response).to have_gitlab_http_status(200) - expect(json_response['commits']).to be_present - expect(json_response['diffs']).to be_present - end - it "compares tags" do - get v3_api(route, current_user), from: 'v1.0.0', to: 'v1.1.0' - expect(response).to have_gitlab_http_status(200) - expect(json_response['commits']).to be_present - expect(json_response['diffs']).to be_present - end - it "compares commits" do - get v3_api(route, current_user), from: sample_commit.id, to: sample_commit.parent_id - expect(response).to have_gitlab_http_status(200) - expect(json_response['commits']).to be_empty - expect(json_response['diffs']).to be_empty - expect(json_response['compare_same_ref']).to be_falsey - end - it "compares commits in reverse order" do - get v3_api(route, current_user), from: sample_commit.parent_id, to: sample_commit.id - expect(response).to have_gitlab_http_status(200) - expect(json_response['commits']).to be_present - expect(json_response['diffs']).to be_present - end - it "compares same refs" do - get v3_api(route, current_user), from: 'master', to: 'master' - expect(response).to have_gitlab_http_status(200) - expect(json_response['commits']).to be_empty - expect(json_response['diffs']).to be_empty - expect(json_response['compare_same_ref']).to be_truthy - end - end - context 'when unauthenticated', 'and project is public' do - it_behaves_like 'repository compare' do - let(:project) { create(:project, :public, :repository) } - let(:current_user) { nil } - end - end - context 'when unauthenticated', 'and project is private' do - it_behaves_like '404 response' do - let(:request) { get v3_api(route) } - let(:message) { '404 Project Not Found' } - end - end - context 'when authenticated', 'as a developer' do - it_behaves_like 'repository compare' do - let(:current_user) { user } - end - end - context 'when authenticated', 'as a guest' do - it_behaves_like '403 response' do - let(:request) { get v3_api(route, guest) } - end - end - end - - describe 'GET /projects/:id/repository/contributors' do - let(:route) { "/projects/#{project.id}/repository/contributors" } - - shared_examples_for 'repository contributors' do - it 'returns valid data' do - get v3_api(route, current_user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - - first_contributor = json_response.first - expect(first_contributor['email']).to eq('tiagonbotelho@hotmail.com') - expect(first_contributor['name']).to eq('tiagonbotelho') - expect(first_contributor['commits']).to eq(1) - expect(first_contributor['additions']).to eq(0) - expect(first_contributor['deletions']).to eq(0) - end - end - - context 'when unauthenticated', 'and project is public' do - it_behaves_like 'repository contributors' do - let(:project) { create(:project, :public, :repository) } - let(:current_user) { nil } - end - end - - context 'when unauthenticated', 'and project is private' do - it_behaves_like '404 response' do - let(:request) { get v3_api(route) } - let(:message) { '404 Project Not Found' } - end - end - - context 'when authenticated', 'as a developer' do - it_behaves_like 'repository contributors' do - let(:current_user) { user } - end - end - - context 'when authenticated', 'as a guest' do - it_behaves_like '403 response' do - let(:request) { get v3_api(route, guest) } - end - end - end -end diff --git a/spec/requests/api/v3/runners_spec.rb b/spec/requests/api/v3/runners_spec.rb deleted file mode 100644 index c91b097a3c7..00000000000 --- a/spec/requests/api/v3/runners_spec.rb +++ /dev/null @@ -1,152 +0,0 @@ -require 'spec_helper' - -describe API::V3::Runners do - let(:admin) { create(:user, :admin) } - let(:user) { create(:user) } - let(:user2) { create(:user) } - - let(:project) { create(:project, creator_id: user.id) } - let(:project2) { create(:project, creator_id: user.id) } - - let!(:shared_runner) { create(:ci_runner, :shared) } - let!(:unused_specific_runner) { create(:ci_runner) } - - let!(:specific_runner) do - create(:ci_runner).tap do |runner| - create(:ci_runner_project, runner: runner, project: project) - end - end - - let!(:two_projects_runner) do - create(:ci_runner).tap do |runner| - create(:ci_runner_project, runner: runner, project: project) - create(:ci_runner_project, runner: runner, project: project2) - end - end - - before do - # Set project access for users - create(:project_member, :master, user: user, project: project) - create(:project_member, :reporter, user: user2, project: project) - end - - describe 'DELETE /runners/:id' do - context 'admin user' do - context 'when runner is shared' do - it 'deletes runner' do - expect do - delete v3_api("/runners/#{shared_runner.id}", admin) - - expect(response).to have_gitlab_http_status(200) - end.to change { Ci::Runner.shared.count }.by(-1) - end - end - - context 'when runner is not shared' do - it 'deletes unused runner' do - expect do - delete v3_api("/runners/#{unused_specific_runner.id}", admin) - - expect(response).to have_gitlab_http_status(200) - end.to change { Ci::Runner.specific.count }.by(-1) - end - - it 'deletes used runner' do - expect do - delete v3_api("/runners/#{specific_runner.id}", admin) - - expect(response).to have_gitlab_http_status(200) - end.to change { Ci::Runner.specific.count }.by(-1) - end - end - - it 'returns 404 if runner does not exists' do - delete v3_api('/runners/9999', admin) - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'authorized user' do - context 'when runner is shared' do - it 'does not delete runner' do - delete v3_api("/runners/#{shared_runner.id}", user) - expect(response).to have_gitlab_http_status(403) - end - end - - context 'when runner is not shared' do - it 'does not delete runner without access to it' do - delete v3_api("/runners/#{specific_runner.id}", user2) - expect(response).to have_gitlab_http_status(403) - end - - it 'does not delete runner with more than one associated project' do - delete v3_api("/runners/#{two_projects_runner.id}", user) - expect(response).to have_gitlab_http_status(403) - end - - it 'deletes runner for one owned project' do - expect do - delete v3_api("/runners/#{specific_runner.id}", user) - - expect(response).to have_gitlab_http_status(200) - end.to change { Ci::Runner.specific.count }.by(-1) - end - end - end - - context 'unauthorized user' do - it 'does not delete runner' do - delete v3_api("/runners/#{specific_runner.id}") - - expect(response).to have_gitlab_http_status(401) - end - end - end - - describe 'DELETE /projects/:id/runners/:runner_id' do - context 'authorized user' do - context 'when runner have more than one associated projects' do - it "disables project's runner" do - expect do - delete v3_api("/projects/#{project.id}/runners/#{two_projects_runner.id}", user) - - expect(response).to have_gitlab_http_status(200) - end.to change { project.runners.count }.by(-1) - end - end - - context 'when runner have one associated projects' do - it "does not disable project's runner" do - expect do - delete v3_api("/projects/#{project.id}/runners/#{specific_runner.id}", user) - end.to change { project.runners.count }.by(0) - expect(response).to have_gitlab_http_status(403) - end - end - - it 'returns 404 is runner is not found' do - delete v3_api("/projects/#{project.id}/runners/9999", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'authorized user without permissions' do - it "does not disable project's runner" do - delete v3_api("/projects/#{project.id}/runners/#{specific_runner.id}", user2) - - expect(response).to have_gitlab_http_status(403) - end - end - - context 'unauthorized user' do - it "does not disable project's runner" do - delete v3_api("/projects/#{project.id}/runners/#{specific_runner.id}") - - expect(response).to have_gitlab_http_status(401) - end - end - end -end diff --git a/spec/requests/api/v3/services_spec.rb b/spec/requests/api/v3/services_spec.rb deleted file mode 100644 index c69a7d58ca6..00000000000 --- a/spec/requests/api/v3/services_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -require "spec_helper" - -describe API::V3::Services do - let(:user) { create(:user) } - let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } - - available_services = Service.available_services_names - available_services.delete('prometheus') - available_services.each do |service| - describe "DELETE /projects/:id/services/#{service.dasherize}" do - include_context service - - before do - initialize_service(service) - end - - it "deletes #{service}" do - delete v3_api("/projects/#{project.id}/services/#{dashed_service}", user) - - expect(response).to have_gitlab_http_status(200) - project.send(service_method).reload - expect(project.send(service_method).activated?).to be_falsey - end - end - end -end diff --git a/spec/requests/api/v3/settings_spec.rb b/spec/requests/api/v3/settings_spec.rb deleted file mode 100644 index 985bfbfa09c..00000000000 --- a/spec/requests/api/v3/settings_spec.rb +++ /dev/null @@ -1,63 +0,0 @@ -require 'spec_helper' - -describe API::V3::Settings, 'Settings' do - let(:user) { create(:user) } - let(:admin) { create(:admin) } - - describe "GET /application/settings" do - it "returns application settings" do - get v3_api("/application/settings", admin) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Hash - expect(json_response['default_projects_limit']).to eq(42) - expect(json_response['password_authentication_enabled']).to be_truthy - expect(json_response['repository_storage']).to eq('default') - expect(json_response['koding_enabled']).to be_falsey - expect(json_response['koding_url']).to be_nil - expect(json_response['plantuml_enabled']).to be_falsey - expect(json_response['plantuml_url']).to be_nil - end - end - - describe "PUT /application/settings" do - context "custom repository storage type set in the config" do - before do - storages = { 'custom' => 'tmp/tests/custom_repositories' } - allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) - end - - it "updates application settings" do - put v3_api("/application/settings", admin), - default_projects_limit: 3, password_authentication_enabled_for_web: false, repository_storage: 'custom', koding_enabled: true, koding_url: 'http://koding.example.com', - plantuml_enabled: true, plantuml_url: 'http://plantuml.example.com' - expect(response).to have_gitlab_http_status(200) - expect(json_response['default_projects_limit']).to eq(3) - expect(json_response['password_authentication_enabled_for_web']).to be_falsey - expect(json_response['repository_storage']).to eq('custom') - expect(json_response['repository_storages']).to eq(['custom']) - expect(json_response['koding_enabled']).to be_truthy - expect(json_response['koding_url']).to eq('http://koding.example.com') - expect(json_response['plantuml_enabled']).to be_truthy - expect(json_response['plantuml_url']).to eq('http://plantuml.example.com') - end - end - - context "missing koding_url value when koding_enabled is true" do - it "returns a blank parameter error message" do - put v3_api("/application/settings", admin), koding_enabled: true - - expect(response).to have_gitlab_http_status(400) - expect(json_response['error']).to eq('koding_url is missing') - end - end - - context "missing plantuml_url value when plantuml_enabled is true" do - it "returns a blank parameter error message" do - put v3_api("/application/settings", admin), plantuml_enabled: true - - expect(response).to have_gitlab_http_status(400) - expect(json_response['error']).to eq('plantuml_url is missing') - end - end - end -end diff --git a/spec/requests/api/v3/snippets_spec.rb b/spec/requests/api/v3/snippets_spec.rb deleted file mode 100644 index e8913039194..00000000000 --- a/spec/requests/api/v3/snippets_spec.rb +++ /dev/null @@ -1,186 +0,0 @@ -require 'rails_helper' - -describe API::V3::Snippets do - let!(:user) { create(:user) } - - describe 'GET /snippets/' do - it 'returns snippets available' do - public_snippet = create(:personal_snippet, :public, author: user) - private_snippet = create(:personal_snippet, :private, author: user) - internal_snippet = create(:personal_snippet, :internal, author: user) - - get v3_api("/snippets/", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly( - public_snippet.id, - internal_snippet.id, - private_snippet.id) - expect(json_response.last).to have_key('web_url') - expect(json_response.last).to have_key('raw_url') - end - - it 'hides private snippets from regular user' do - create(:personal_snippet, :private) - - get v3_api("/snippets/", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response.size).to eq(0) - end - end - - describe 'GET /snippets/public' do - let!(:other_user) { create(:user) } - let!(:public_snippet) { create(:personal_snippet, :public, author: user) } - let!(:private_snippet) { create(:personal_snippet, :private, author: user) } - let!(:internal_snippet) { create(:personal_snippet, :internal, author: user) } - let!(:public_snippet_other) { create(:personal_snippet, :public, author: other_user) } - let!(:private_snippet_other) { create(:personal_snippet, :private, author: other_user) } - let!(:internal_snippet_other) { create(:personal_snippet, :internal, author: other_user) } - - it 'returns all snippets with public visibility from all users' do - get v3_api("/snippets/public", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly( - public_snippet.id, - public_snippet_other.id) - expect(json_response.map { |snippet| snippet['web_url']} ).to include( - "http://localhost/snippets/#{public_snippet.id}", - "http://localhost/snippets/#{public_snippet_other.id}") - expect(json_response.map { |snippet| snippet['raw_url']} ).to include( - "http://localhost/snippets/#{public_snippet.id}/raw", - "http://localhost/snippets/#{public_snippet_other.id}/raw") - end - end - - describe 'GET /snippets/:id/raw' do - let(:snippet) { create(:personal_snippet, author: user) } - - it 'returns raw text' do - get v3_api("/snippets/#{snippet.id}/raw", user) - - expect(response).to have_gitlab_http_status(200) - expect(response.content_type).to eq 'text/plain' - expect(response.body).to eq(snippet.content) - end - - it 'returns 404 for invalid snippet id' do - delete v3_api("/snippets/1234", user) - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Snippet Not Found') - end - end - - describe 'POST /snippets/' do - let(:params) do - { - title: 'Test Title', - file_name: 'test.rb', - content: 'puts "hello world"', - visibility_level: Snippet::PUBLIC - } - end - - it 'creates a new snippet' do - expect do - post v3_api("/snippets/", user), params - end.to change { PersonalSnippet.count }.by(1) - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq(params[:title]) - expect(json_response['file_name']).to eq(params[:file_name]) - end - - it 'returns 400 for missing parameters' do - params.delete(:title) - - post v3_api("/snippets/", user), params - - expect(response).to have_gitlab_http_status(400) - end - - context 'when the snippet is spam' do - def create_snippet(snippet_params = {}) - post v3_api('/snippets', user), params.merge(snippet_params) - end - - before do - allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true) - end - - context 'when the snippet is private' do - it 'creates the snippet' do - expect { create_snippet(visibility_level: Snippet::PRIVATE) } - .to change { Snippet.count }.by(1) - end - end - - context 'when the snippet is public' do - it 'rejects the shippet' do - expect { create_snippet(visibility_level: Snippet::PUBLIC) } - .not_to change { Snippet.count } - expect(response).to have_gitlab_http_status(400) - end - - it 'creates a spam log' do - expect { create_snippet(visibility_level: Snippet::PUBLIC) } - .to change { SpamLog.count }.by(1) - end - end - end - end - - describe 'PUT /snippets/:id' do - let(:other_user) { create(:user) } - let(:public_snippet) { create(:personal_snippet, :public, author: user) } - it 'updates snippet' do - new_content = 'New content' - - put v3_api("/snippets/#{public_snippet.id}", user), content: new_content - - expect(response).to have_gitlab_http_status(200) - public_snippet.reload - expect(public_snippet.content).to eq(new_content) - end - - it 'returns 404 for invalid snippet id' do - put v3_api("/snippets/1234", user), title: 'foo' - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Snippet Not Found') - end - - it "returns 404 for another user's snippet" do - put v3_api("/snippets/#{public_snippet.id}", other_user), title: 'fubar' - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Snippet Not Found') - end - - it 'returns 400 for missing parameters' do - put v3_api("/snippets/1234", user) - - expect(response).to have_gitlab_http_status(400) - end - end - - describe 'DELETE /snippets/:id' do - let!(:public_snippet) { create(:personal_snippet, :public, author: user) } - it 'deletes snippet' do - expect do - delete v3_api("/snippets/#{public_snippet.id}", user) - - expect(response).to have_gitlab_http_status(204) - end.to change { PersonalSnippet.count }.by(-1) - end - - it 'returns 404 for invalid snippet id' do - delete v3_api("/snippets/1234", user) - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Snippet Not Found') - end - end -end diff --git a/spec/requests/api/v3/system_hooks_spec.rb b/spec/requests/api/v3/system_hooks_spec.rb deleted file mode 100644 index 30711c60faa..00000000000 --- a/spec/requests/api/v3/system_hooks_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -require 'spec_helper' - -describe API::V3::SystemHooks do - let(:user) { create(:user) } - let(:admin) { create(:admin) } - let!(:hook) { create(:system_hook, url: "http://example.com") } - - before { stub_request(:post, hook.url) } - - describe "GET /hooks" do - context "when no user" do - it "returns authentication error" do - get v3_api("/hooks") - - expect(response).to have_gitlab_http_status(401) - end - end - - context "when not an admin" do - it "returns forbidden error" do - get v3_api("/hooks", user) - - expect(response).to have_gitlab_http_status(403) - end - end - - context "when authenticated as admin" do - it "returns an array of hooks" do - get v3_api("/hooks", admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['url']).to eq(hook.url) - expect(json_response.first['push_events']).to be false - expect(json_response.first['tag_push_events']).to be false - expect(json_response.first['repository_update_events']).to be true - end - end - end - - describe "DELETE /hooks/:id" do - it "deletes a hook" do - expect do - delete v3_api("/hooks/#{hook.id}", admin) - - expect(response).to have_gitlab_http_status(200) - end.to change { SystemHook.count }.by(-1) - end - - it 'returns 404 if the system hook does not exist' do - delete v3_api('/hooks/12345', admin) - - expect(response).to have_gitlab_http_status(404) - end - end -end diff --git a/spec/requests/api/v3/tags_spec.rb b/spec/requests/api/v3/tags_spec.rb deleted file mode 100644 index e6ad005fa87..00000000000 --- a/spec/requests/api/v3/tags_spec.rb +++ /dev/null @@ -1,88 +0,0 @@ -require 'spec_helper' -require 'mime/types' - -describe API::V3::Tags do - include RepoHelpers - - let(:user) { create(:user) } - let(:user2) { create(:user) } - let!(:project) { create(:project, :repository, creator: user) } - let!(:master) { create(:project_member, :master, user: user, project: project) } - - describe "GET /projects/:id/repository/tags" do - let(:tag_name) { project.repository.tag_names.sort.reverse.first } - let(:description) { 'Awesome release!' } - - shared_examples_for 'repository tags' do - it 'returns the repository tags' do - get v3_api("/projects/#{project.id}/repository/tags", current_user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['name']).to eq(tag_name) - end - end - - context 'when unauthenticated' do - it_behaves_like 'repository tags' do - let(:project) { create(:project, :public, :repository) } - let(:current_user) { nil } - end - end - - context 'when authenticated' do - it_behaves_like 'repository tags' do - let(:current_user) { user } - end - end - - context 'without releases' do - it "returns an array of project tags" do - get v3_api("/projects/#{project.id}/repository/tags", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['name']).to eq(tag_name) - end - end - - context 'with releases' do - before do - release = project.releases.find_or_initialize_by(tag: tag_name) - release.update_attributes(description: description) - end - - it "returns an array of project tags with release info" do - get v3_api("/projects/#{project.id}/repository/tags", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['name']).to eq(tag_name) - expect(json_response.first['message']).to eq('Version 1.1.0') - expect(json_response.first['release']['description']).to eq(description) - end - end - end - - describe 'DELETE /projects/:id/repository/tags/:tag_name' do - let(:tag_name) { project.repository.tag_names.sort.reverse.first } - - before do - allow_any_instance_of(Repository).to receive(:rm_tag).and_return(true) - end - - context 'delete tag' do - it 'deletes an existing tag' do - delete v3_api("/projects/#{project.id}/repository/tags/#{tag_name}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['tag_name']).to eq(tag_name) - end - - it 'raises 404 if the tag does not exist' do - delete v3_api("/projects/#{project.id}/repository/tags/foobar", user) - expect(response).to have_gitlab_http_status(404) - end - end - end -end diff --git a/spec/requests/api/v3/templates_spec.rb b/spec/requests/api/v3/templates_spec.rb deleted file mode 100644 index 1a637f3cf96..00000000000 --- a/spec/requests/api/v3/templates_spec.rb +++ /dev/null @@ -1,201 +0,0 @@ -require 'spec_helper' - -describe API::V3::Templates do - shared_examples_for 'the Template Entity' do |path| - before { get v3_api(path) } - - it { expect(json_response['name']).to eq('Ruby') } - it { expect(json_response['content']).to include('*.gem') } - end - - shared_examples_for 'the TemplateList Entity' do |path| - before { get v3_api(path) } - - it { expect(json_response.first['name']).not_to be_nil } - it { expect(json_response.first['content']).to be_nil } - end - - shared_examples_for 'requesting gitignores' do |path| - it 'returns a list of available gitignore templates' do - get v3_api(path) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to be > 15 - end - end - - shared_examples_for 'requesting gitlab-ci-ymls' do |path| - it 'returns a list of available gitlab_ci_ymls' do - get v3_api(path) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['name']).not_to be_nil - end - end - - shared_examples_for 'requesting gitlab-ci-yml for Ruby' do |path| - it 'adds a disclaimer on the top' do - get v3_api(path) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['content']).to start_with("# This file is a template,") - end - end - - shared_examples_for 'the License Template Entity' do |path| - before { get v3_api(path) } - - it 'returns a license template' do - expect(json_response['key']).to eq('mit') - expect(json_response['name']).to eq('MIT License') - expect(json_response['nickname']).to be_nil - expect(json_response['popular']).to be true - expect(json_response['html_url']).to eq('http://choosealicense.com/licenses/mit/') - expect(json_response['source_url']).to eq('https://opensource.org/licenses/MIT') - expect(json_response['description']).to include('A short and simple permissive license with conditions') - expect(json_response['conditions']).to eq(%w[include-copyright]) - expect(json_response['permissions']).to eq(%w[commercial-use modifications distribution private-use]) - expect(json_response['limitations']).to eq(%w[liability warranty]) - expect(json_response['content']).to include('MIT License') - end - end - - shared_examples_for 'GET licenses' do |path| - it 'returns a list of available license templates' do - get v3_api(path) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to eq(12) - expect(json_response.map { |l| l['key'] }).to include('agpl-3.0') - end - - describe 'the popular parameter' do - context 'with popular=1' do - it 'returns a list of available popular license templates' do - get v3_api("#{path}?popular=1") - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to eq(3) - expect(json_response.map { |l| l['key'] }).to include('apache-2.0') - end - end - end - end - - shared_examples_for 'GET licenses/:name' do |path| - context 'with :project and :fullname given' do - before do - get v3_api("#{path}/#{license_type}?project=My+Awesome+Project&fullname=Anton+#{license_type.upcase}") - end - - context 'for the mit license' do - let(:license_type) { 'mit' } - - it 'returns the license text' do - expect(json_response['content']).to include('MIT License') - end - - it 'replaces placeholder values' do - expect(json_response['content']).to include("Copyright (c) #{Time.now.year} Anton") - end - end - - context 'for the agpl-3.0 license' do - let(:license_type) { 'agpl-3.0' } - - it 'returns the license text' do - expect(json_response['content']).to include('GNU AFFERO GENERAL PUBLIC LICENSE') - end - - it 'replaces placeholder values' do - expect(json_response['content']).to include('My Awesome Project') - expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton") - end - end - - context 'for the gpl-3.0 license' do - let(:license_type) { 'gpl-3.0' } - - it 'returns the license text' do - expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE') - end - - it 'replaces placeholder values' do - expect(json_response['content']).to include('My Awesome Project') - expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton") - end - end - - context 'for the gpl-2.0 license' do - let(:license_type) { 'gpl-2.0' } - - it 'returns the license text' do - expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE') - end - - it 'replaces placeholder values' do - expect(json_response['content']).to include('My Awesome Project') - expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton") - end - end - - context 'for the apache-2.0 license' do - let(:license_type) { 'apache-2.0' } - - it 'returns the license text' do - expect(json_response['content']).to include('Apache License') - end - - it 'replaces placeholder values' do - expect(json_response['content']).to include("Copyright #{Time.now.year} Anton") - end - end - - context 'for an uknown license' do - let(:license_type) { 'muth-over9000' } - - it 'returns a 404' do - expect(response).to have_gitlab_http_status(404) - end - end - end - - context 'with no :fullname given' do - context 'with an authenticated user' do - let(:user) { create(:user) } - - it 'replaces the copyright owner placeholder with the name of the current user' do - get v3_api('/templates/licenses/mit', user) - - expect(json_response['content']).to include("Copyright (c) #{Time.now.year} #{user.name}") - end - end - end - end - - describe 'with /templates namespace' do - it_behaves_like 'the Template Entity', '/templates/gitignores/Ruby' - it_behaves_like 'the TemplateList Entity', '/templates/gitignores' - it_behaves_like 'requesting gitignores', '/templates/gitignores' - it_behaves_like 'requesting gitlab-ci-ymls', '/templates/gitlab_ci_ymls' - it_behaves_like 'requesting gitlab-ci-yml for Ruby', '/templates/gitlab_ci_ymls/Ruby' - it_behaves_like 'the License Template Entity', '/templates/licenses/mit' - it_behaves_like 'GET licenses', '/templates/licenses' - it_behaves_like 'GET licenses/:name', '/templates/licenses' - end - - describe 'without /templates namespace' do - it_behaves_like 'the Template Entity', '/gitignores/Ruby' - it_behaves_like 'the TemplateList Entity', '/gitignores' - it_behaves_like 'requesting gitignores', '/gitignores' - it_behaves_like 'requesting gitlab-ci-ymls', '/gitlab_ci_ymls' - it_behaves_like 'requesting gitlab-ci-yml for Ruby', '/gitlab_ci_ymls/Ruby' - it_behaves_like 'the License Template Entity', '/licenses/mit' - it_behaves_like 'GET licenses', '/licenses' - it_behaves_like 'GET licenses/:name', '/licenses' - end -end diff --git a/spec/requests/api/v3/todos_spec.rb b/spec/requests/api/v3/todos_spec.rb deleted file mode 100644 index ea648e3917f..00000000000 --- a/spec/requests/api/v3/todos_spec.rb +++ /dev/null @@ -1,77 +0,0 @@ -require 'spec_helper' - -describe API::V3::Todos do - let(:project_1) { create(:project) } - let(:project_2) { create(:project) } - let(:author_1) { create(:user) } - let(:author_2) { create(:user) } - let(:john_doe) { create(:user, username: 'john_doe') } - let!(:pending_1) { create(:todo, :mentioned, project: project_1, author: author_1, user: john_doe) } - let!(:pending_2) { create(:todo, project: project_2, author: author_2, user: john_doe) } - let!(:pending_3) { create(:todo, project: project_1, author: author_2, user: john_doe) } - let!(:done) { create(:todo, :done, project: project_1, author: author_1, user: john_doe) } - - before do - project_1.add_developer(john_doe) - project_2.add_developer(john_doe) - end - - describe 'DELETE /todos/:id' do - context 'when unauthenticated' do - it 'returns authentication error' do - delete v3_api("/todos/#{pending_1.id}") - - expect(response.status).to eq(401) - end - end - - context 'when authenticated' do - it 'marks a todo as done' do - delete v3_api("/todos/#{pending_1.id}", john_doe) - - expect(response.status).to eq(200) - expect(pending_1.reload).to be_done - end - - it 'updates todos cache' do - expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original - - delete v3_api("/todos/#{pending_1.id}", john_doe) - end - - it 'returns 404 if the todo does not belong to the current user' do - delete v3_api("/todos/#{pending_1.id}", author_1) - - expect(response.status).to eq(404) - end - end - end - - describe 'DELETE /todos' do - context 'when unauthenticated' do - it 'returns authentication error' do - delete v3_api('/todos') - - expect(response.status).to eq(401) - end - end - - context 'when authenticated' do - it 'marks all todos as done' do - delete v3_api('/todos', john_doe) - - expect(response.status).to eq(200) - expect(response.body).to eq('3') - expect(pending_1.reload).to be_done - expect(pending_2.reload).to be_done - expect(pending_3.reload).to be_done - end - - it 'updates todos cache' do - expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original - - delete v3_api("/todos", john_doe) - end - end - end -end diff --git a/spec/requests/api/v3/triggers_spec.rb b/spec/requests/api/v3/triggers_spec.rb deleted file mode 100644 index e8e2f49d7a0..00000000000 --- a/spec/requests/api/v3/triggers_spec.rb +++ /dev/null @@ -1,235 +0,0 @@ -require 'spec_helper' - -describe API::V3::Triggers do - let(:user) { create(:user) } - let(:user2) { create(:user) } - let!(:trigger_token) { 'secure_token' } - let!(:project) { create(:project, :repository, creator: user) } - let!(:master) { create(:project_member, :master, user: user, project: project) } - let!(:developer) { create(:project_member, :developer, user: user2, project: project) } - - let!(:trigger) do - create(:ci_trigger, project: project, token: trigger_token, owner: user) - end - - describe 'POST /projects/:project_id/trigger' do - let!(:project2) { create(:project) } - let(:options) do - { - token: trigger_token - } - end - - before do - stub_ci_pipeline_to_return_yaml_file - end - - context 'Handles errors' do - it 'returns bad request if token is missing' do - post v3_api("/projects/#{project.id}/trigger/builds"), ref: 'master' - expect(response).to have_gitlab_http_status(400) - end - - it 'returns not found if project is not found' do - post v3_api('/projects/0/trigger/builds'), options.merge(ref: 'master') - expect(response).to have_gitlab_http_status(404) - end - - it 'returns unauthorized if token is for different project' do - post v3_api("/projects/#{project2.id}/trigger/builds"), options.merge(ref: 'master') - expect(response).to have_gitlab_http_status(404) - end - end - - context 'Have a commit' do - let(:pipeline) { project.pipelines.last } - - it 'creates builds' do - post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'master') - expect(response).to have_gitlab_http_status(201) - pipeline.builds.reload - expect(pipeline.builds.pending.size).to eq(2) - expect(pipeline.builds.size).to eq(5) - end - - it 'returns bad request with no builds created if there\'s no commit for that ref' do - post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'other-branch') - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']['base']) - .to contain_exactly('Reference not found') - end - - context 'Validates variables' do - let(:variables) do - { 'TRIGGER_KEY' => 'TRIGGER_VALUE' } - end - - it 'validates variables to be a hash' do - post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(variables: 'value', ref: 'master') - expect(response).to have_gitlab_http_status(400) - expect(json_response['error']).to eq('variables is invalid') - end - - it 'validates variables needs to be a map of key-valued strings' do - post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(variables: { key: %w(1 2) }, ref: 'master') - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']).to eq('variables needs to be a map of key-valued strings') - end - - it 'creates trigger request with variables' do - post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(variables: variables, ref: 'master') - expect(response).to have_gitlab_http_status(201) - pipeline.builds.reload - expect(pipeline.variables.map { |v| { v.key => v.value } }.first).to eq(variables) - expect(json_response['variables']).to eq(variables) - end - end - end - - context 'when triggering a pipeline from a trigger token' do - it 'creates builds from the ref given in the URL, not in the body' do - expect do - post v3_api("/projects/#{project.id}/ref/master/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' } - end.to change(project.builds, :count).by(5) - expect(response).to have_gitlab_http_status(201) - end - - context 'when ref contains a dot' do - it 'creates builds from the ref given in the URL, not in the body' do - project.repository.create_file(user, '.gitlab/gitlabhq/new_feature.md', 'something valid', message: 'new_feature', branch_name: 'v.1-branch') - - expect do - post v3_api("/projects/#{project.id}/ref/v.1-branch/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' } - end.to change(project.builds, :count).by(4) - - expect(response).to have_gitlab_http_status(201) - end - end - end - end - - describe 'GET /projects/:id/triggers' do - context 'authenticated user with valid permissions' do - it 'returns list of triggers' do - get v3_api("/projects/#{project.id}/triggers", user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_a(Array) - expect(json_response[0]).to have_key('token') - end - end - - context 'authenticated user with invalid permissions' do - it 'does not return triggers list' do - get v3_api("/projects/#{project.id}/triggers", user2) - - expect(response).to have_gitlab_http_status(403) - end - end - - context 'unauthenticated user' do - it 'does not return triggers list' do - get v3_api("/projects/#{project.id}/triggers") - - expect(response).to have_gitlab_http_status(401) - end - end - end - - describe 'GET /projects/:id/triggers/:token' do - context 'authenticated user with valid permissions' do - it 'returns trigger details' do - get v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_a(Hash) - end - - it 'responds with 404 Not Found if requesting non-existing trigger' do - get v3_api("/projects/#{project.id}/triggers/abcdef012345", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'authenticated user with invalid permissions' do - it 'does not return triggers list' do - get v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user2) - - expect(response).to have_gitlab_http_status(403) - end - end - - context 'unauthenticated user' do - it 'does not return triggers list' do - get v3_api("/projects/#{project.id}/triggers/#{trigger.token}") - - expect(response).to have_gitlab_http_status(401) - end - end - end - - describe 'POST /projects/:id/triggers' do - context 'authenticated user with valid permissions' do - it 'creates trigger' do - expect do - post v3_api("/projects/#{project.id}/triggers", user) - end.to change {project.triggers.count}.by(1) - - expect(response).to have_gitlab_http_status(201) - expect(json_response).to be_a(Hash) - end - end - - context 'authenticated user with invalid permissions' do - it 'does not create trigger' do - post v3_api("/projects/#{project.id}/triggers", user2) - - expect(response).to have_gitlab_http_status(403) - end - end - - context 'unauthenticated user' do - it 'does not create trigger' do - post v3_api("/projects/#{project.id}/triggers") - - expect(response).to have_gitlab_http_status(401) - end - end - end - - describe 'DELETE /projects/:id/triggers/:token' do - context 'authenticated user with valid permissions' do - it 'deletes trigger' do - expect do - delete v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user) - - expect(response).to have_gitlab_http_status(200) - end.to change {project.triggers.count}.by(-1) - end - - it 'responds with 404 Not Found if requesting non-existing trigger' do - delete v3_api("/projects/#{project.id}/triggers/abcdef012345", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'authenticated user with invalid permissions' do - it 'does not delete trigger' do - delete v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user2) - - expect(response).to have_gitlab_http_status(403) - end - end - - context 'unauthenticated user' do - it 'does not delete trigger' do - delete v3_api("/projects/#{project.id}/triggers/#{trigger.token}") - - expect(response).to have_gitlab_http_status(401) - end - end - end -end diff --git a/spec/requests/api/v3/users_spec.rb b/spec/requests/api/v3/users_spec.rb deleted file mode 100644 index bbd05f240d2..00000000000 --- a/spec/requests/api/v3/users_spec.rb +++ /dev/null @@ -1,362 +0,0 @@ -require 'spec_helper' - -describe API::V3::Users do - let(:user) { create(:user) } - let(:admin) { create(:admin) } - let(:key) { create(:key, user: user) } - let(:email) { create(:email, user: user) } - let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') } - - describe 'GET /users' do - context 'when authenticated' do - it 'returns an array of users' do - get v3_api('/users', user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - username = user.username - expect(json_response.detect do |user| - user['username'] == username - end['username']).to eq(username) - end - end - - context 'when authenticated as user' do - it 'does not reveal the `is_admin` flag of the user' do - get v3_api('/users', user) - - expect(json_response.first.keys).not_to include 'is_admin' - end - end - - context 'when authenticated as admin' do - it 'reveals the `is_admin` flag of the user' do - get v3_api('/users', admin) - - expect(json_response.first.keys).to include 'is_admin' - end - end - end - - describe 'GET /user/:id/keys' do - before { admin } - - context 'when unauthenticated' do - it 'returns authentication error' do - get v3_api("/users/#{user.id}/keys") - expect(response).to have_gitlab_http_status(401) - end - end - - context 'when authenticated' do - it 'returns 404 for non-existing user' do - get v3_api('/users/999999/keys', admin) - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 User Not Found') - end - - it 'returns array of ssh keys' do - user.keys << key - user.save - - get v3_api("/users/#{user.id}/keys", admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['title']).to eq(key.title) - end - end - - context "scopes" do - let(:user) { admin } - let(:path) { "/users/#{user.id}/keys" } - let(:api_call) { method(:v3_api) } - - before do - user.keys << key - user.save - end - - include_examples 'allows the "read_user" scope' - end - end - - describe 'GET /user/:id/emails' do - before { admin } - - context 'when unauthenticated' do - it 'returns authentication error' do - get v3_api("/users/#{user.id}/emails") - expect(response).to have_gitlab_http_status(401) - end - end - - context 'when authenticated' do - it 'returns 404 for non-existing user' do - get v3_api('/users/999999/emails', admin) - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 User Not Found') - end - - it 'returns array of emails' do - user.emails << email - user.save - - get v3_api("/users/#{user.id}/emails", admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['email']).to eq(email.email) - end - - it "returns a 404 for invalid ID" do - put v3_api("/users/ASDF/emails", admin) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe "GET /user/keys" do - context "when unauthenticated" do - it "returns authentication error" do - get v3_api("/user/keys") - expect(response).to have_gitlab_http_status(401) - end - end - - context "when authenticated" do - it "returns array of ssh keys" do - user.keys << key - user.save - - get v3_api("/user/keys", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first["title"]).to eq(key.title) - end - end - end - - describe "GET /user/emails" do - context "when unauthenticated" do - it "returns authentication error" do - get v3_api("/user/emails") - expect(response).to have_gitlab_http_status(401) - end - end - - context "when authenticated" do - it "returns array of emails" do - user.emails << email - user.save - - get v3_api("/user/emails", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first["email"]).to eq(email.email) - end - end - end - - describe 'PUT /users/:id/block' do - before { admin } - it 'blocks existing user' do - put v3_api("/users/#{user.id}/block", admin) - expect(response).to have_gitlab_http_status(200) - expect(user.reload.state).to eq('blocked') - end - - it 'does not re-block ldap blocked users' do - put v3_api("/users/#{ldap_blocked_user.id}/block", admin) - expect(response).to have_gitlab_http_status(403) - expect(ldap_blocked_user.reload.state).to eq('ldap_blocked') - end - - it 'does not be available for non admin users' do - put v3_api("/users/#{user.id}/block", user) - expect(response).to have_gitlab_http_status(403) - expect(user.reload.state).to eq('active') - end - - it 'returns a 404 error if user id not found' do - put v3_api('/users/9999/block', admin) - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 User Not Found') - end - end - - describe 'PUT /users/:id/unblock' do - let(:blocked_user) { create(:user, state: 'blocked') } - before { admin } - - it 'unblocks existing user' do - put v3_api("/users/#{user.id}/unblock", admin) - expect(response).to have_gitlab_http_status(200) - expect(user.reload.state).to eq('active') - end - - it 'unblocks a blocked user' do - put v3_api("/users/#{blocked_user.id}/unblock", admin) - expect(response).to have_gitlab_http_status(200) - expect(blocked_user.reload.state).to eq('active') - end - - it 'does not unblock ldap blocked users' do - put v3_api("/users/#{ldap_blocked_user.id}/unblock", admin) - expect(response).to have_gitlab_http_status(403) - expect(ldap_blocked_user.reload.state).to eq('ldap_blocked') - end - - it 'does not be available for non admin users' do - put v3_api("/users/#{user.id}/unblock", user) - expect(response).to have_gitlab_http_status(403) - expect(user.reload.state).to eq('active') - end - - it 'returns a 404 error if user id not found' do - put v3_api('/users/9999/block', admin) - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 User Not Found') - end - - it "returns a 404 for invalid ID" do - put v3_api("/users/ASDF/block", admin) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'GET /users/:id/events' do - let(:user) { create(:user) } - let(:project) { create(:project) } - let(:note) { create(:note_on_issue, note: 'What an awesome day!', project: project) } - - before do - project.add_user(user, :developer) - EventCreateService.new.leave_note(note, user) - end - - context "as a user than cannot see the event's project" do - it 'returns no events' do - other_user = create(:user) - - get api("/users/#{user.id}/events", other_user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_empty - end - end - - context "as a user than can see the event's project" do - context 'when the list of events includes push events' do - let(:event) { create(:push_event, author: user, project: project) } - let!(:payload) { create(:push_event_payload, event: event) } - let(:payload_hash) { json_response[0]['push_data'] } - - before do - get api("/users/#{user.id}/events?action=pushed", user) - end - - it 'responds with HTTP 200 OK' do - expect(response).to have_gitlab_http_status(200) - end - - it 'includes the push payload as a Hash' do - expect(payload_hash).to be_an_instance_of(Hash) - end - - it 'includes the push payload details' do - expect(payload_hash['commit_count']).to eq(payload.commit_count) - expect(payload_hash['action']).to eq(payload.action) - expect(payload_hash['ref_type']).to eq(payload.ref_type) - expect(payload_hash['commit_to']).to eq(payload.commit_to) - end - end - - context 'joined event' do - it 'returns the "joined" event' do - get v3_api("/users/#{user.id}/events", user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - - comment_event = json_response.find { |e| e['action_name'] == 'commented on' } - - expect(comment_event['project_id'].to_i).to eq(project.id) - expect(comment_event['author_username']).to eq(user.username) - expect(comment_event['note']['id']).to eq(note.id) - expect(comment_event['note']['body']).to eq('What an awesome day!') - - joined_event = json_response.find { |e| e['action_name'] == 'joined' } - - expect(joined_event['project_id'].to_i).to eq(project.id) - expect(joined_event['author_username']).to eq(user.username) - expect(joined_event['author']['name']).to eq(user.name) - end - end - - context 'when there are multiple events from different projects' do - let(:second_note) { create(:note_on_issue, project: create(:project)) } - let(:third_note) { create(:note_on_issue, project: project) } - - before do - second_note.project.add_user(user, :developer) - - [second_note, third_note].each do |note| - EventCreateService.new.leave_note(note, user) - end - end - - it 'returns events in the correct order (from newest to oldest)' do - get v3_api("/users/#{user.id}/events", user) - - comment_events = json_response.select { |e| e['action_name'] == 'commented on' } - - expect(comment_events[0]['target_id']).to eq(third_note.id) - expect(comment_events[1]['target_id']).to eq(second_note.id) - expect(comment_events[2]['target_id']).to eq(note.id) - end - end - end - - it 'returns a 404 error if not found' do - get v3_api('/users/420/events', user) - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 User Not Found') - end - end - - describe 'POST /users' do - it 'creates confirmed user when confirm parameter is false' do - optional_attributes = { confirm: false } - attributes = attributes_for(:user).merge(optional_attributes) - - post v3_api('/users', admin), attributes - - user_id = json_response['id'] - new_user = User.find(user_id) - - expect(new_user).to be_confirmed - end - - it 'does not reveal the `is_admin` flag of the user' do - post v3_api('/users', admin), attributes_for(:user) - - expect(json_response['is_admin']).to be_nil - end - - context "scopes" do - let(:user) { admin } - let(:path) { '/users' } - let(:api_call) { method(:v3_api) } - - include_examples 'does not allow the "read_user" scope' - end - end -end diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb index b18e922b063..c0a3ea397df 100644 --- a/spec/requests/rack_attack_global_spec.rb +++ b/spec/requests/rack_attack_global_spec.rb @@ -349,7 +349,7 @@ describe 'Rack Attack global throttles' do end def rss_url(user) - "/dashboard/projects.atom?rss_token=#{user.rss_token}" + "/dashboard/projects.atom?feed_token=#{user.feed_token}" end def private_token_headers(user) diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 9345671a1a7..dd8f6239587 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -162,8 +162,8 @@ describe ProfilesController, "routing" do expect(get("/profile/audit_log")).to route_to('profiles#audit_log') end - it "to #reset_rss_token" do - expect(put("/profile/reset_rss_token")).to route_to('profiles#reset_rss_token') + it "to #reset_feed_token" do + expect(put("/profile/reset_feed_token")).to route_to('profiles#reset_feed_token') end it "to #show" do @@ -249,7 +249,11 @@ describe DashboardController, "routing" do end it "to #issues" do - expect(get("/dashboard/issues")).to route_to('dashboard#issues') + expect(get("/dashboard/issues.html")).to route_to('dashboard#issues', format: 'html') + end + + it "to #calendar_issues" do + expect(get("/dashboard/issues.ics")).to route_to('dashboard#issues_calendar', format: 'ics') end it "to #merge_requests" do diff --git a/spec/serializers/job_entity_spec.rb b/spec/serializers/job_entity_spec.rb index c90396ebb28..a5581a34517 100644 --- a/spec/serializers/job_entity_spec.rb +++ b/spec/serializers/job_entity_spec.rb @@ -131,7 +131,7 @@ describe JobEntity do end context 'when job failed' do - let(:job) { create(:ci_build, :script_failure) } + let(:job) { create(:ci_build, :api_failure) } it 'contains details' do expect(subject[:status]).to include :icon, :favicon, :text, :label, :tooltip @@ -142,20 +142,20 @@ describe JobEntity do end it 'should indicate the failure reason on tooltip' do - expect(subject[:status][:tooltip]).to eq('failed <br> (script failure)') + expect(subject[:status][:tooltip]).to eq('failed <br> (API failure)') end it 'should include a callout message with a verbose output' do - expect(subject[:callout_message]).to eq('There has been a script failure. Check the job log for more information') + expect(subject[:callout_message]).to eq('There has been an API failure, please try again') end it 'should state that it is not recoverable' do - expect(subject[:recoverable]).to be_falsy + expect(subject[:recoverable]).to be_truthy end end context 'when job is allowed to fail' do - let(:job) { create(:ci_build, :allowed_to_fail, :script_failure) } + let(:job) { create(:ci_build, :allowed_to_fail, :api_failure) } it 'contains details' do expect(subject[:status]).to include :icon, :favicon, :text, :label, :tooltip @@ -166,15 +166,24 @@ describe JobEntity do end it 'should indicate the failure reason on tooltip' do - expect(subject[:status][:tooltip]).to eq('failed <br> (script failure) (allowed to fail)') + expect(subject[:status][:tooltip]).to eq('failed <br> (API failure) (allowed to fail)') end it 'should include a callout message with a verbose output' do - expect(subject[:callout_message]).to eq('There has been a script failure. Check the job log for more information') + expect(subject[:callout_message]).to eq('There has been an API failure, please try again') end it 'should state that it is not recoverable' do - expect(subject[:recoverable]).to be_falsy + expect(subject[:recoverable]).to be_truthy + end + end + + context 'when the job failed with a script failure' do + let(:job) { create(:ci_build, :failed, :script_failure) } + + it 'should not include callout message or recoverable keys' do + expect(subject).not_to include('callout_message') + expect(subject).not_to include('recoverable') end end diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index e8568bf8bb3..dc30a9bccc1 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -249,24 +249,58 @@ describe MergeRequests::MergeService do expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message)) end - context "when fast-forward merge is not allowed" do + context 'when squashing' do before do - allow_any_instance_of(Repository).to receive(:ancestor?).and_return(nil) + merge_request.update!(source_branch: 'master', target_branch: 'feature') end - %w(semi-linear ff).each do |merge_method| - it "logs and saves error if merge is #{merge_method} only" do - merge_method = 'rebase_merge' if merge_method == 'semi-linear' - merge_request.project.update(merge_method: merge_method) - error_message = 'Only fast-forward merge is allowed for your project. Please update your source branch' - allow(service).to receive(:execute_hooks) + it 'logs and saves error if there is an error when squashing' do + error_message = 'Failed to squash. Should be done manually' - service.execute(merge_request) + allow_any_instance_of(MergeRequests::SquashService).to receive(:squash).and_return(nil) + merge_request.update(squash: true) + + service.execute(merge_request) + + expect(merge_request).to be_open + expect(merge_request.merge_commit_sha).to be_nil + expect(merge_request.merge_error).to include(error_message) + expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message)) + end + + it 'logs and saves error if there is a squash in progress' do + error_message = 'another squash is already in progress' + + allow_any_instance_of(MergeRequest).to receive(:squash_in_progress?).and_return(true) + merge_request.update(squash: true) + + service.execute(merge_request) - expect(merge_request).to be_open - expect(merge_request.merge_commit_sha).to be_nil - expect(merge_request.merge_error).to include(error_message) - expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message)) + expect(merge_request).to be_open + expect(merge_request.merge_commit_sha).to be_nil + expect(merge_request.merge_error).to include(error_message) + expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message)) + end + + context "when fast-forward merge is not allowed" do + before do + allow_any_instance_of(Repository).to receive(:ancestor?).and_return(nil) + end + + %w(semi-linear ff).each do |merge_method| + it "logs and saves error if merge is #{merge_method} only" do + merge_method = 'rebase_merge' if merge_method == 'semi-linear' + merge_request.project.update(merge_method: merge_method) + error_message = 'Only fast-forward merge is allowed for your project. Please update your source branch' + allow(service).to receive(:execute_hooks) + + service.execute(merge_request) + + expect(merge_request).to be_open + expect(merge_request.merge_commit_sha).to be_nil + expect(merge_request.merge_error).to include(error_message) + expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message)) + end end end end diff --git a/spec/services/merge_requests/squash_service_spec.rb b/spec/services/merge_requests/squash_service_spec.rb new file mode 100644 index 00000000000..bd884787425 --- /dev/null +++ b/spec/services/merge_requests/squash_service_spec.rb @@ -0,0 +1,199 @@ +require 'spec_helper' + +describe MergeRequests::SquashService do + let(:service) { described_class.new(project, user, {}) } + let(:user) { project.owner } + let(:project) { create(:project, :repository) } + let(:repository) { project.repository.raw } + let(:log_error) { "Failed to squash merge request #{merge_request.to_reference(full: true)}:" } + let(:squash_dir_path) do + File.join(Gitlab.config.shared.path, 'tmp/squash', repository.gl_repository, merge_request.id.to_s) + end + let(:merge_request_with_one_commit) do + create(:merge_request, + source_branch: 'feature', source_project: project, + target_branch: 'master', target_project: project) + end + + let(:merge_request_with_only_new_files) do + create(:merge_request, + source_branch: 'video', source_project: project, + target_branch: 'master', target_project: project) + end + + let(:merge_request_with_large_files) do + create(:merge_request, + source_branch: 'squash-large-files', source_project: project, + target_branch: 'master', target_project: project) + end + + shared_examples 'the squash succeeds' do + it 'returns the squashed commit SHA' do + result = service.execute(merge_request) + + expect(result).to match(status: :success, squash_sha: a_string_matching(/\h{40}/)) + expect(result[:squash_sha]).not_to eq(merge_request.diff_head_sha) + end + + it 'cleans up the temporary directory' do + service.execute(merge_request) + + expect(File.exist?(squash_dir_path)).to be(false) + end + + it 'does not keep the branch push event' do + expect { service.execute(merge_request) }.not_to change { Event.count } + end + + context 'the squashed commit' do + let(:squash_sha) { service.execute(merge_request)[:squash_sha] } + let(:squash_commit) { project.repository.commit(squash_sha) } + + it 'copies the author info and message from the merge request' do + expect(squash_commit.author_name).to eq(merge_request.author.name) + expect(squash_commit.author_email).to eq(merge_request.author.email) + + # Commit messages have a trailing newline, but titles don't. + expect(squash_commit.message.chomp).to eq(merge_request.title) + end + + it 'sets the current user as the committer' do + expect(squash_commit.committer_name).to eq(user.name.chomp('.')) + expect(squash_commit.committer_email).to eq(user.email) + end + + it 'has the same diff as the merge request, but a different SHA' do + rugged = project.repository.rugged + mr_diff = rugged.diff(merge_request.diff_base_sha, merge_request.diff_head_sha) + squash_diff = rugged.diff(merge_request.diff_start_sha, squash_sha) + + expect(squash_diff.patch.length).to eq(mr_diff.patch.length) + expect(squash_commit.sha).not_to eq(merge_request.diff_head_sha) + end + end + end + + describe '#execute' do + context 'when there is only one commit in the merge request' do + it 'returns that commit SHA' do + result = service.execute(merge_request_with_one_commit) + + expect(result).to match(status: :success, squash_sha: merge_request_with_one_commit.diff_head_sha) + end + + it 'does not perform any git actions' do + expect(repository).not_to receive(:popen) + + service.execute(merge_request_with_one_commit) + end + end + + context 'when squashing only new files' do + let(:merge_request) { merge_request_with_only_new_files } + + include_examples 'the squash succeeds' + end + + context 'when squashing with files too large to display' do + let(:merge_request) { merge_request_with_large_files } + + include_examples 'the squash succeeds' + end + + context 'git errors' do + let(:merge_request) { merge_request_with_only_new_files } + let(:error) { 'A test error' } + + context 'with gitaly enabled' do + before do + allow(repository.gitaly_operation_client).to receive(:user_squash) + .and_raise(Gitlab::Git::Repository::GitError, error) + end + + it 'logs the stage and output' do + expect(service).to receive(:log_error).with(log_error) + expect(service).to receive(:log_error).with(error) + + service.execute(merge_request) + end + + it 'returns an error' do + expect(service.execute(merge_request)).to match(status: :error, + message: a_string_including('squash')) + end + end + + context 'with Gitaly disabled', :skip_gitaly_mock do + stages = { + 'add worktree for squash' => 'worktree', + 'configure sparse checkout' => 'config', + 'get files in diff' => 'diff --name-only', + 'check out target branch' => 'checkout', + 'apply patch' => 'diff --binary', + 'commit squashed changes' => 'commit', + 'get SHA of squashed commit' => 'rev-parse' + } + + stages.each do |stage, command| + context "when the #{stage} stage fails" do + before do + git_command = a_collection_containing_exactly( + a_string_starting_with("#{Gitlab.config.git.bin_path} #{command}") + ).or( + a_collection_starting_with([Gitlab.config.git.bin_path] + command.split) + ) + + allow(repository).to receive(:popen).and_return(['', 0]) + allow(repository).to receive(:popen).with(git_command, anything, anything, anything).and_return([error, 1]) + end + + it 'logs the stage and output' do + expect(service).to receive(:log_error).with(log_error) + expect(service).to receive(:log_error).with(error) + + service.execute(merge_request) + end + + it 'returns an error' do + expect(service.execute(merge_request)).to match(status: :error, + message: a_string_including('squash')) + end + + it 'cleans up the temporary directory' do + expect(File.exist?(squash_dir_path)).to be(false) + + service.execute(merge_request) + end + end + end + end + end + + context 'when any other exception is thrown' do + let(:merge_request) { merge_request_with_only_new_files } + let(:error) { 'A test error' } + + before do + allow(merge_request).to receive(:commits_count).and_raise(error) + end + + it 'logs the MR reference and exception' do + expect(service).to receive(:log_error).with(a_string_including("#{project.full_path}#{merge_request.to_reference}")) + expect(service).to receive(:log_error).with(error) + + service.execute(merge_request) + end + + it 'returns an error' do + expect(service.execute(merge_request)).to match(status: :error, + message: a_string_including('squash')) + end + + it 'cleans up the temporary directory' do + service.execute(merge_request) + + expect(File.exist?(squash_dir_path)).to be(false) + end + end + end +end diff --git a/spec/support/api/v3/time_tracking_shared_examples.rb b/spec/support/api/v3/time_tracking_shared_examples.rb deleted file mode 100644 index f27a2d06c83..00000000000 --- a/spec/support/api/v3/time_tracking_shared_examples.rb +++ /dev/null @@ -1,128 +0,0 @@ -shared_examples 'V3 time tracking endpoints' do |issuable_name| - issuable_collection_name = issuable_name.pluralize - - describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_estimate" do - context 'with an unauthorized user' do - subject { post(v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", non_member), duration: '1w') } - - it_behaves_like 'an unauthorized API user' - end - - it "sets the time estimate for #{issuable_name}" do - post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['human_time_estimate']).to eq('1w') - end - - describe 'updating the current estimate' do - before do - post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w' - end - - context 'when duration has a bad format' do - it 'does not modify the original estimate' do - post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: 'foo' - - expect(response).to have_gitlab_http_status(400) - expect(issuable.reload.human_time_estimate).to eq('1w') - end - end - - context 'with a valid duration' do - it 'updates the estimate' do - post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '3w1h' - - expect(response).to have_gitlab_http_status(200) - expect(issuable.reload.human_time_estimate).to eq('3w 1h') - end - end - end - end - - describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_time_estimate" do - context 'with an unauthorized user' do - subject { post(v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", non_member)) } - - it_behaves_like 'an unauthorized API user' - end - - it "resets the time estimate for #{issuable_name}" do - post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['time_estimate']).to eq(0) - end - end - - describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/add_spent_time" do - context 'with an unauthorized user' do - subject do - post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", non_member), - duration: '2h' - end - - it_behaves_like 'an unauthorized API user' - end - - it "add spent time for #{issuable_name}" do - post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user), - duration: '2h' - - expect(response).to have_gitlab_http_status(201) - expect(json_response['human_total_time_spent']).to eq('2h') - end - - context 'when subtracting time' do - it 'subtracts time of the total spent time' do - issuable.update_attributes!(spend_time: { duration: 7200, user_id: user.id }) - - post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user), - duration: '-1h' - - expect(response).to have_gitlab_http_status(201) - expect(json_response['total_time_spent']).to eq(3600) - end - end - - context 'when time to subtract is greater than the total spent time' do - it 'does not modify the total time spent' do - issuable.update_attributes!(spend_time: { duration: 7200, user_id: user.id }) - - post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user), - duration: '-1w' - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']['time_spent'].first).to match(/exceeds the total time spent/) - end - end - end - - describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_spent_time" do - context 'with an unauthorized user' do - subject { post(v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", non_member)) } - - it_behaves_like 'an unauthorized API user' - end - - it "resets spent time for #{issuable_name}" do - post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['total_time_spent']).to eq(0) - end - end - - describe "GET /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_stats" do - it "returns the time stats for #{issuable_name}" do - issuable.update_attributes!(spend_time: { duration: 1800, user_id: user.id }, - time_estimate: 3600) - - get v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_stats", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['total_time_spent']).to eq(1800) - expect(json_response['time_estimate']).to eq(3600) - end - end -end diff --git a/spec/support/features/rss_shared_examples.rb b/spec/support/features/rss_shared_examples.rb index 50fbbc7f55b..0de92aedba5 100644 --- a/spec/support/features/rss_shared_examples.rb +++ b/spec/support/features/rss_shared_examples.rb @@ -1,23 +1,23 @@ -shared_examples "an autodiscoverable RSS feed with current_user's RSS token" do - it "has an RSS autodiscovery link tag with current_user's RSS token" do - expect(page).to have_css("link[type*='atom+xml'][href*='rss_token=#{user.rss_token}']", visible: false) +shared_examples "an autodiscoverable RSS feed with current_user's feed token" do + it "has an RSS autodiscovery link tag with current_user's feed token" do + expect(page).to have_css("link[type*='atom+xml'][href*='feed_token=#{user.feed_token}']", visible: false) end end -shared_examples "it has an RSS button with current_user's RSS token" do - it "shows the RSS button with current_user's RSS token" do - expect(page).to have_css("a:has(.fa-rss)[href*='rss_token=#{user.rss_token}']") +shared_examples "it has an RSS button with current_user's feed token" do + it "shows the RSS button with current_user's feed token" do + expect(page).to have_css("a:has(.fa-rss)[href*='feed_token=#{user.feed_token}']") end end -shared_examples "an autodiscoverable RSS feed without an RSS token" do - it "has an RSS autodiscovery link tag without an RSS token" do - expect(page).to have_css("link[type*='atom+xml']:not([href*='rss_token'])", visible: false) +shared_examples "an autodiscoverable RSS feed without a feed token" do + it "has an RSS autodiscovery link tag without a feed token" do + expect(page).to have_css("link[type*='atom+xml']:not([href*='feed_token'])", visible: false) end end -shared_examples "it has an RSS button without an RSS token" do - it "shows the RSS button without an RSS token" do - expect(page).to have_css("a:has(.fa-rss):not([href*='rss_token'])") +shared_examples "it has an RSS button without a feed token" do + it "shows the RSS button without a feed token" do + expect(page).to have_css("a:has(.fa-rss):not([href*='feed_token'])") end end diff --git a/spec/support/gitlab-git-test.git/packed-refs b/spec/support/gitlab-git-test.git/packed-refs index ea50e4ad3f6..6a61e5df267 100644 --- a/spec/support/gitlab-git-test.git/packed-refs +++ b/spec/support/gitlab-git-test.git/packed-refs @@ -9,6 +9,7 @@ 4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6 refs/heads/master 5937ac0a7beb003549fc5fd26fc247adbce4a52e refs/heads/merge-test 9596bc54a6f0c0c98248fe97077eb5ccf48a98d0 refs/heads/missing-gitmodules +4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6 refs/heads/Ääh-test-utf-8 f4e6814c3e4e7a0de82a9e7cd20c626cc963a2f8 refs/tags/v1.0.0 ^6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b refs/tags/v1.1.0 diff --git a/spec/support/gitlab_stubs/project_8.json b/spec/support/gitlab_stubs/project_8.json index f0a9fce859c..81b08ab8288 100644 --- a/spec/support/gitlab_stubs/project_8.json +++ b/spec/support/gitlab_stubs/project_8.json @@ -1,38 +1,38 @@ { - "id":8, - "description":"ssh access and repository management app for GitLab", - "default_branch":"master", - "public":false, - "visibility_level":0, - "ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab-shell.git", - "http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab-shell.git", - "web_url":"http://demo.gitlab.com/gitlab/gitlab-shell", - "owner": { - "id":4, - "name":"GitLab", - "created_at":"2012-12-21T13:03:05Z" - }, - "name":"gitlab-shell", - "name_with_namespace":"GitLab / gitlab-shell", - "path":"gitlab-shell", - "path_with_namespace":"gitlab/gitlab-shell", - "issues_enabled":true, - "merge_requests_enabled":true, - "wall_enabled":false, - "wiki_enabled":true, - "snippets_enabled":false, - "created_at":"2013-03-20T13:28:53Z", - "last_activity_at":"2013-11-30T00:11:17Z", - "namespace":{ - "created_at":"2012-12-21T13:03:05Z", - "description":"Self hosted Git management software", - "id":4, - "name":"GitLab", - "owner_id":1, - "path":"gitlab", - "updated_at":"2013-03-20T13:29:13Z" + "id": 8, + "description": "ssh access and repository management app for GitLab", + "default_branch": "master", + "visibility": "private", + "ssh_url_to_repo": "git@demo.gitlab.com:gitlab/gitlab-shell.git", + "http_url_to_repo": "http://demo.gitlab.com/gitlab/gitlab-shell.git", + "web_url": "http://demo.gitlab.com/gitlab/gitlab-shell", + "owner": { + "id": 4, + "name": "GitLab", + "username": "gitlab", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61", + "web_url": "http://demo.gitlab.com/gitlab" }, - "permissions":{ + "name": "gitlab-shell", + "name_with_namespace": "GitLab / gitlab-shell", + "path": "gitlab-shell", + "path_with_namespace": "gitlab/gitlab-shell", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "snippets_enabled": false, + "created_at": "2013-03-20T13:28:53Z", + "last_activity_at": "2013-11-30T00:11:17Z", + "namespace": { + "id": 4, + "name": "GitLab", + "path": "gitlab", + "kind": "group", + "full_path": "gitlab", + "parent_id": null + }, + "permissions": { "project_access": { "access_level": 10, "notification_level": 3 @@ -42,4 +42,4 @@ "notification_level": 3 } } -}
\ No newline at end of file +} diff --git a/spec/support/gitlab_stubs/projects.json b/spec/support/gitlab_stubs/projects.json index ca42c14c5d8..67ce1acca2c 100644 --- a/spec/support/gitlab_stubs/projects.json +++ b/spec/support/gitlab_stubs/projects.json @@ -1 +1,282 @@ -[{"id":3,"description":"GitLab is open source software to collaborate on code. Create projects and repositories, manage access and do code reviews.","default_branch":"master","public":true,"visibility_level":20,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlabhq.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlabhq.git","web_url":"http://demo.gitlab.com/gitlab/gitlabhq","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlabhq","name_with_namespace":"GitLab / gitlabhq","path":"gitlabhq","path_with_namespace":"gitlab/gitlabhq","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":true,"wiki_enabled":true,"snippets_enabled":true,"created_at":"2012-12-21T13:06:34Z","last_activity_at":"2013-12-02T19:10:10Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":4,"description":"Component of GitLab CI. Web application","default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab-ci.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab-ci.git","web_url":"http://demo.gitlab.com/gitlab/gitlab-ci","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlab-ci","name_with_namespace":"GitLab / gitlab-ci","path":"gitlab-ci","path_with_namespace":"gitlab/gitlab-ci","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":true,"wiki_enabled":true,"snippets_enabled":true,"created_at":"2012-12-21T13:06:50Z","last_activity_at":"2013-11-28T19:26:54Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":5,"description":"","default_branch":"master","public":true,"visibility_level":20,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab-recipes.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab-recipes.git","web_url":"http://demo.gitlab.com/gitlab/gitlab-recipes","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlab-recipes","name_with_namespace":"GitLab / gitlab-recipes","path":"gitlab-recipes","path_with_namespace":"gitlab/gitlab-recipes","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":true,"wiki_enabled":true,"snippets_enabled":true,"created_at":"2012-12-21T13:07:02Z","last_activity_at":"2013-12-02T13:54:10Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":8,"description":"ssh access and repository management app for GitLab","default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab-shell.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab-shell.git","web_url":"http://demo.gitlab.com/gitlab/gitlab-shell","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlab-shell","name_with_namespace":"GitLab / gitlab-shell","path":"gitlab-shell","path_with_namespace":"gitlab/gitlab-shell","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-03-20T13:28:53Z","last_activity_at":"2013-11-30T00:11:17Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":9,"description":null,"default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab_git.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab_git.git","web_url":"http://demo.gitlab.com/gitlab/gitlab_git","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlab_git","name_with_namespace":"GitLab / gitlab_git","path":"gitlab_git","path_with_namespace":"gitlab/gitlab_git","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-04-28T19:15:08Z","last_activity_at":"2013-12-02T13:07:13Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":10,"description":"ultra lite authorization library http://randx.github.com/six/\\r\\n ","default_branch":"master","public":true,"visibility_level":20,"ssh_url_to_repo":"git@demo.gitlab.com:sandbox/six.git","http_url_to_repo":"http://demo.gitlab.com/sandbox/six.git","web_url":"http://demo.gitlab.com/sandbox/six","owner":{"id":8,"name":"Sandbox","created_at":"2013-08-01T16:44:17Z"},"name":"Six","name_with_namespace":"Sandbox / Six","path":"six","path_with_namespace":"sandbox/six","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-08-01T16:45:02Z","last_activity_at":"2013-11-29T11:30:56Z","namespace":{"created_at":"2013-08-01T16:44:17Z","description":"","id":8,"name":"Sandbox","owner_id":1,"path":"sandbox","updated_at":"2013-08-01T16:44:17Z"}},{"id":11,"description":"Simple HTML5 Charts using the <canvas> tag ","default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:sandbox/charts-js.git","http_url_to_repo":"http://demo.gitlab.com/sandbox/charts-js.git","web_url":"http://demo.gitlab.com/sandbox/charts-js","owner":{"id":8,"name":"Sandbox","created_at":"2013-08-01T16:44:17Z"},"name":"Charts.js","name_with_namespace":"Sandbox / Charts.js","path":"charts-js","path_with_namespace":"sandbox/charts-js","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-08-01T16:47:29Z","last_activity_at":"2013-12-02T15:18:11Z","namespace":{"created_at":"2013-08-01T16:44:17Z","description":"","id":8,"name":"Sandbox","owner_id":1,"path":"sandbox","updated_at":"2013-08-01T16:44:17Z"}},{"id":13,"description":"","default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:sandbox/afro.git","http_url_to_repo":"http://demo.gitlab.com/sandbox/afro.git","web_url":"http://demo.gitlab.com/sandbox/afro","owner":{"id":8,"name":"Sandbox","created_at":"2013-08-01T16:44:17Z"},"name":"Afro","name_with_namespace":"Sandbox / Afro","path":"afro","path_with_namespace":"sandbox/afro","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-11-14T17:45:19Z","last_activity_at":"2013-12-02T17:41:45Z","namespace":{"created_at":"2013-08-01T16:44:17Z","description":"","id":8,"name":"Sandbox","owner_id":1,"path":"sandbox","updated_at":"2013-08-01T16:44:17Z"}}]
\ No newline at end of file +[ + { + "id": 3, + "description": "GitLab is open source software to collaborate on code. Create projects and repositories, manage access and do code reviews.", + "default_branch": "master", + "visibility": "public", + "ssh_url_to_repo": "git@demo.gitlab.com:gitlab/gitlabhq.git", + "http_url_to_repo": "http://demo.gitlab.com/gitlab/gitlabhq.git", + "web_url": "http://demo.gitlab.com/gitlab/gitlabhq", + "owner": { + "id": 4, + "name": "GitLab", + "username": "gitlab", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61", + "web_url": "http://demo.gitlab.com/gitlab" + }, + "name": "gitlabhq", + "name_with_namespace": "GitLab / gitlabhq", + "path": "gitlabhq", + "path_with_namespace": "gitlab/gitlabhq", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "snippets_enabled": true, + "created_at": "2012-12-21T13:06:34Z", + "last_activity_at": "2013-12-02T19:10:10Z", + "namespace": { + "id": 4, + "name": "GitLab", + "path": "gitlab", + "kind": "group", + "full_path": "gitlab", + "parent_id": null + } + }, + { + "id": 4, + "description": "Component of GitLab CI. Web application", + "default_branch": "master", + "visibility": "private", + "ssh_url_to_repo": "git@demo.gitlab.com:gitlab/gitlab-ci.git", + "http_url_to_repo": "http://demo.gitlab.com/gitlab/gitlab-ci.git", + "web_url": "http://demo.gitlab.com/gitlab/gitlab-ci", + "owner": { + "id": 4, + "name": "GitLab", + "username": "gitlab", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61", + "web_url": "http://demo.gitlab.com/gitlab" + }, + "name": "gitlab-ci", + "name_with_namespace": "GitLab / gitlab-ci", + "path": "gitlab-ci", + "path_with_namespace": "gitlab/gitlab-ci", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "snippets_enabled": true, + "created_at": "2012-12-21T13:06:50Z", + "last_activity_at": "2013-11-28T19:26:54Z", + "namespace": { + "id": 4, + "name": "GitLab", + "path": "gitlab", + "kind": "group", + "full_path": "gitlab", + "parent_id": null + } + }, + { + "id": 5, + "description": "", + "default_branch": "master", + "visibility": "public", + "ssh_url_to_repo": "git@demo.gitlab.com:gitlab/gitlab-recipes.git", + "http_url_to_repo": "http://demo.gitlab.com/gitlab/gitlab-recipes.git", + "web_url": "http://demo.gitlab.com/gitlab/gitlab-recipes", + "owner": { + "id": 4, + "name": "GitLab", + "username": "gitlab", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61", + "web_url": "http://demo.gitlab.com/gitlab" + }, + "name": "gitlab-recipes", + "name_with_namespace": "GitLab / gitlab-recipes", + "path": "gitlab-recipes", + "path_with_namespace": "gitlab/gitlab-recipes", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "snippets_enabled": true, + "created_at": "2012-12-21T13:07:02Z", + "last_activity_at": "2013-12-02T13:54:10Z", + "namespace": { + "id": 4, + "name": "GitLab", + "path": "gitlab", + "kind": "group", + "full_path": "gitlab", + "parent_id": null + } + }, + { + "id": 8, + "description": "ssh access and repository management app for GitLab", + "default_branch": "master", + "visibility": "private", + "ssh_url_to_repo": "git@demo.gitlab.com:gitlab/gitlab-shell.git", + "http_url_to_repo": "http://demo.gitlab.com/gitlab/gitlab-shell.git", + "web_url": "http://demo.gitlab.com/gitlab/gitlab-shell", + "owner": { + "id": 4, + "name": "GitLab", + "username": "gitlab", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61", + "web_url": "http://demo.gitlab.com/gitlab" + }, + "name": "gitlab-shell", + "name_with_namespace": "GitLab / gitlab-shell", + "path": "gitlab-shell", + "path_with_namespace": "gitlab/gitlab-shell", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "snippets_enabled": false, + "created_at": "2013-03-20T13:28:53Z", + "last_activity_at": "2013-11-30T00:11:17Z", + "namespace": { + "id": 4, + "name": "GitLab", + "path": "gitlab", + "kind": "group", + "full_path": "gitlab", + "parent_id": null + } + }, + { + "id": 9, + "description": null, + "default_branch": "master", + "visibility": "private", + "ssh_url_to_repo": "git@demo.gitlab.com:gitlab/gitlab_git.git", + "http_url_to_repo": "http://demo.gitlab.com/gitlab/gitlab_git.git", + "web_url": "http://demo.gitlab.com/gitlab/gitlab_git", + "owner": { + "id": 4, + "name": "GitLab", + "username": "gitlab", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61", + "web_url": "http://demo.gitlab.com/gitlab" + }, + "name": "gitlab_git", + "name_with_namespace": "GitLab / gitlab_git", + "path": "gitlab_git", + "path_with_namespace": "gitlab/gitlab_git", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "snippets_enabled": false, + "created_at": "2013-04-28T19:15:08Z", + "last_activity_at": "2013-12-02T13:07:13Z", + "namespace": { + "id": 4, + "name": "GitLab", + "path": "gitlab", + "kind": "group", + "full_path": "gitlab", + "parent_id": null + } + }, + { + "id": 10, + "description": "ultra lite authorization library http://randx.github.com/six/\\r\\n ", + "default_branch": "master", + "visibility": "public", + "ssh_url_to_repo": "git@demo.gitlab.com:sandbox/six.git", + "http_url_to_repo": "http://demo.gitlab.com/sandbox/six.git", + "web_url": "http://demo.gitlab.com/sandbox/six", + "owner": { + "id": 4, + "name": "GitLab", + "username": "gitlab", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61", + "web_url": "http://demo.gitlab.com/gitlab" + }, + "name": "Six", + "name_with_namespace": "Sandbox / Six", + "path": "six", + "path_with_namespace": "sandbox/six", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "snippets_enabled": false, + "created_at": "2013-08-01T16:45:02Z", + "last_activity_at": "2013-11-29T11:30:56Z", + "namespace": { + "id": 4, + "name": "GitLab", + "path": "gitlab", + "kind": "group", + "full_path": "gitlab", + "parent_id": null + } + }, + { + "id": 11, + "description": "Simple HTML5 Charts using the <canvas> tag ", + "default_branch": "master", + "visibility": "private", + "ssh_url_to_repo": "git@demo.gitlab.com:sandbox/charts-js.git", + "http_url_to_repo": "http://demo.gitlab.com/sandbox/charts-js.git", + "web_url": "http://demo.gitlab.com/sandbox/charts-js", + "owner": { + "id": 4, + "name": "GitLab", + "username": "gitlab", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61", + "web_url": "http://demo.gitlab.com/gitlab" + }, + "name": "Charts.js", + "name_with_namespace": "Sandbox / Charts.js", + "path": "charts-js", + "path_with_namespace": "sandbox/charts-js", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "snippets_enabled": false, + "created_at": "2013-08-01T16:47:29Z", + "last_activity_at": "2013-12-02T15:18:11Z", + "namespace": { + "id": 4, + "name": "GitLab", + "path": "gitlab", + "kind": "group", + "full_path": "gitlab", + "parent_id": null + } + }, + { + "id": 13, + "description": "", + "default_branch": "master", + "visibility": "private", + "ssh_url_to_repo": "git@demo.gitlab.com:sandbox/afro.git", + "http_url_to_repo": "http://demo.gitlab.com/sandbox/afro.git", + "web_url": "http://demo.gitlab.com/sandbox/afro", + "owner": { + "id": 4, + "name": "GitLab", + "username": "gitlab", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61", + "web_url": "http://demo.gitlab.com/gitlab" + }, + "name": "Afro", + "name_with_namespace": "Sandbox / Afro", + "path": "afro", + "path_with_namespace": "sandbox/afro", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "snippets_enabled": false, + "created_at": "2013-11-14T17:45:19Z", + "last_activity_at": "2013-12-02T17:41:45Z", + "namespace": { + "id": 4, + "name": "GitLab", + "path": "gitlab", + "kind": "group", + "full_path": "gitlab", + "parent_id": null + } + } +] diff --git a/spec/support/gitlab_stubs/session.json b/spec/support/gitlab_stubs/session.json deleted file mode 100644 index 658ff5871b0..00000000000 --- a/spec/support/gitlab_stubs/session.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id":2, - "username":"jsmith", - "email":"test@test.com", - "name":"John Smith", - "bio":"", - "skype":"aertert", - "linkedin":"", - "twitter":"", - "theme_id":2,"color_scheme_id":2, - "state":"active", - "created_at":"2012-12-21T13:02:20Z", - "extern_uid":null, - "provider":null, - "is_admin":false, - "can_create_group":false, - "can_create_project":false -} diff --git a/spec/support/helpers/api_helpers.rb b/spec/support/helpers/api_helpers.rb index ac0c7a9b493..a57a3b2cf34 100644 --- a/spec/support/helpers/api_helpers.rb +++ b/spec/support/helpers/api_helpers.rb @@ -36,15 +36,4 @@ module ApiHelpers full_path end - - # Temporary helper method for simplifying V3 exclusive API specs - def v3_api(path, user = nil, personal_access_token: nil, oauth_access_token: nil) - api( - path, - user, - version: 'v3', - personal_access_token: personal_access_token, - oauth_access_token: oauth_access_token - ) - end end diff --git a/spec/support/helpers/seed_repo.rb b/spec/support/helpers/seed_repo.rb index b4868e82cd7..71f1a86b0c1 100644 --- a/spec/support/helpers/seed_repo.rb +++ b/spec/support/helpers/seed_repo.rb @@ -98,6 +98,7 @@ module SeedRepo master merge-test missing-gitmodules + Ääh-test-utf-8 ].freeze TAGS = %w[ v1.0.0 diff --git a/spec/support/helpers/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb index c1618f5086c..2933f2c78dc 100644 --- a/spec/support/helpers/stub_gitlab_calls.rb +++ b/spec/support/helpers/stub_gitlab_calls.rb @@ -1,6 +1,5 @@ module StubGitlabCalls def stub_gitlab_calls - stub_session stub_user stub_project_8 stub_project_8_hooks @@ -71,23 +70,14 @@ module StubGitlabCalls Gitlab.config.gitlab.url end - def stub_session - f = File.read(Rails.root.join('spec/support/gitlab_stubs/session.json')) - - stub_request(:post, "#{gitlab_url}api/v3/session.json") - .with(body: "{\"email\":\"test@test.com\",\"password\":\"123456\"}", - headers: { 'Content-Type' => 'application/json' }) - .to_return(status: 201, body: f, headers: { 'Content-Type' => 'application/json' }) - end - def stub_user f = File.read(Rails.root.join('spec/support/gitlab_stubs/user.json')) - stub_request(:get, "#{gitlab_url}api/v3/user?private_token=Wvjy2Krpb7y8xi93owUz") + stub_request(:get, "#{gitlab_url}api/v4/user?private_token=Wvjy2Krpb7y8xi93owUz") .with(headers: { 'Content-Type' => 'application/json' }) .to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' }) - stub_request(:get, "#{gitlab_url}api/v3/user?access_token=some_token") + stub_request(:get, "#{gitlab_url}api/v4/user?access_token=some_token") .with(headers: { 'Content-Type' => 'application/json' }) .to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' }) end @@ -105,19 +95,19 @@ module StubGitlabCalls def stub_projects f = File.read(Rails.root.join('spec/support/gitlab_stubs/projects.json')) - stub_request(:get, "#{gitlab_url}api/v3/projects.json?archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz") + stub_request(:get, "#{gitlab_url}api/v4/projects.json?archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz") .with(headers: { 'Content-Type' => 'application/json' }) .to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' }) end def stub_projects_owned - stub_request(:get, "#{gitlab_url}api/v3/projects/owned.json?archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz") + stub_request(:get, "#{gitlab_url}api/v4/projects?owned=true&archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz") .with(headers: { 'Content-Type' => 'application/json' }) .to_return(status: 200, body: "", headers: {}) end def stub_ci_enable - stub_request(:put, "#{gitlab_url}api/v3/projects/2/services/gitlab-ci.json?private_token=Wvjy2Krpb7y8xi93owUz") + stub_request(:put, "#{gitlab_url}api/v4/projects/2/services/gitlab-ci.json?private_token=Wvjy2Krpb7y8xi93owUz") .with(headers: { 'Content-Type' => 'application/json' }) .to_return(status: 200, body: "", headers: {}) end diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index 57aa07cf4fa..1fef50a52ec 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -47,6 +47,7 @@ module TestEnv 'v1.1.0' => 'b83d6e3', 'add-ipython-files' => '93ee732', 'add-pdf-file' => 'e774ebd', + 'squash-large-files' => '54cec52', 'add-pdf-text-binary' => '79faa7b', 'add_images_and_changes' => '010d106' }.freeze diff --git a/spec/support/import_export/configuration_helper.rb b/spec/support/import_export/configuration_helper.rb index f752508d48c..bbac6ca6a9c 100644 --- a/spec/support/import_export/configuration_helper.rb +++ b/spec/support/import_export/configuration_helper.rb @@ -10,7 +10,7 @@ module ConfigurationHelper def relation_class_for_name(relation_name) relation_name = Gitlab::ImportExport::RelationFactory::OVERRIDES[relation_name.to_sym] || relation_name - relation_name.to_s.classify.constantize + Gitlab::ImportExport::RelationFactory.relation_class(relation_name) end def parsed_attributes(relation_name, attributes) diff --git a/spec/support/shared_examples/requests/api/merge_requests_list.rb b/spec/support/shared_examples/requests/api/merge_requests_list.rb new file mode 100644 index 00000000000..d5e22b8cb56 --- /dev/null +++ b/spec/support/shared_examples/requests/api/merge_requests_list.rb @@ -0,0 +1,280 @@ +shared_examples 'merge requests list' do + context 'when unauthenticated' do + it 'returns merge requests for public projects' do + get api(endpoint_path) + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to be_an Array + end + end + + context 'when authenticated' do + it 'avoids N+1 queries' do + control = ActiveRecord::QueryRecorder.new do + get api(endpoint_path, user) + end + + create(:merge_request, state: 'closed', milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: 'Test', created_at: base_time) + + create(:merge_request, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: 'Test', created_at: base_time) + + expect do + get api(endpoint_path, user) + end.not_to exceed_query_limit(control) + end + + it 'returns an array of all merge_requests' do + get api(endpoint_path, user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.last['title']).to eq(merge_request.title) + expect(json_response.last).to have_key('web_url') + expect(json_response.last['sha']).to eq(merge_request.diff_head_sha) + expect(json_response.last['merge_commit_sha']).to be_nil + expect(json_response.last['merge_commit_sha']).to eq(merge_request.merge_commit_sha) + expect(json_response.last['downvotes']).to eq(1) + expect(json_response.last['upvotes']).to eq(1) + expect(json_response.last['labels']).to eq([label2.title, label.title]) + expect(json_response.first['title']).to eq(merge_request_merged.title) + expect(json_response.first['sha']).to eq(merge_request_merged.diff_head_sha) + expect(json_response.first['merge_commit_sha']).not_to be_nil + expect(json_response.first['merge_commit_sha']).to eq(merge_request_merged.merge_commit_sha) + end + + it 'returns an array of all merge_requests using simple mode' do + path = endpoint_path + '?view=simple' + + get api(path, user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response.last.keys).to match_array(%w(id iid title web_url created_at description project_id state updated_at)) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.last['iid']).to eq(merge_request.iid) + expect(json_response.last['title']).to eq(merge_request.title) + expect(json_response.last).to have_key('web_url') + expect(json_response.first['iid']).to eq(merge_request_merged.iid) + expect(json_response.first['title']).to eq(merge_request_merged.title) + expect(json_response.first).to have_key('web_url') + end + + it 'returns an array of all merge_requests' do + path = endpoint_path + '?state' + + get api(path, user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.last['title']).to eq(merge_request.title) + end + + it 'returns an array of open merge_requests' do + path = endpoint_path + '?state=opened' + + get api(path, user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.last['title']).to eq(merge_request.title) + end + + it 'returns an array of closed merge_requests' do + path = endpoint_path + '?state=closed' + + get api(path, user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['title']).to eq(merge_request_closed.title) + end + + it 'returns an array of merged merge_requests' do + path = endpoint_path + '?state=merged' + + get api(path, user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['title']).to eq(merge_request_merged.title) + end + + it 'matches V4 response schema' do + get api(endpoint_path, user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/merge_requests') + end + + it 'returns an empty array if no issue matches milestone' do + get api(endpoint_path, user), milestone: '1.0.0' + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns an empty array if milestone does not exist' do + get api(endpoint_path, user), milestone: 'foo' + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns an array of merge requests in given milestone' do + get api(endpoint_path, user), milestone: '0.9' + + expect(json_response.first['title']).to eq merge_request_closed.title + expect(json_response.first['id']).to eq merge_request_closed.id + end + + it 'returns an array of merge requests matching state in milestone' do + get api(endpoint_path, user), milestone: '0.9', state: 'closed' + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(merge_request_closed.id) + end + + it 'returns an array of labeled merge requests' do + path = endpoint_path + "?labels=#{label.title}" + + get api(path, user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['labels']).to eq([label2.title, label.title]) + end + + it 'returns an array of labeled merge requests where all labels match' do + path = endpoint_path + "?labels=#{label.title},foo,bar" + + get api(path, user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns an empty array if no merge request matches labels' do + path = endpoint_path + '?labels=foo,bar' + + get api(path, user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns an array of labeled merge requests that are merged for a milestone' do + bug_label = create(:label, title: 'bug', color: '#FFAABB', project: project) + + mr1 = create(:merge_request, state: 'merged', source_project: project, target_project: project, milestone: milestone) + mr2 = create(:merge_request, state: 'merged', source_project: project, target_project: project, milestone: milestone1) + mr3 = create(:merge_request, state: 'closed', source_project: project, target_project: project, milestone: milestone1) + _mr = create(:merge_request, state: 'merged', source_project: project, target_project: project, milestone: milestone1) + + create(:label_link, label: bug_label, target: mr1) + create(:label_link, label: bug_label, target: mr2) + create(:label_link, label: bug_label, target: mr3) + + path = endpoint_path + "?labels=#{bug_label.title}&milestone=#{milestone1.title}&state=merged" + + get api(path, user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(mr2.id) + end + + context 'with ordering' do + before do + @mr_later = mr_with_later_created_and_updated_at_time + @mr_earlier = mr_with_earlier_created_and_updated_at_time + end + + it 'returns an array of merge_requests in ascending order' do + path = endpoint_path + '?sort=asc' + + get api(path, user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + response_dates = json_response.map { |merge_request| merge_request['created_at'] } + expect(response_dates).to eq(response_dates.sort) + end + + it 'returns an array of merge_requests in descending order' do + path = endpoint_path + '?sort=desc' + + get api(path, user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + response_dates = json_response.map { |merge_request| merge_request['created_at'] } + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it 'returns an array of merge_requests ordered by updated_at' do + path = endpoint_path + '?order_by=updated_at' + + get api(path, user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + response_dates = json_response.map { |merge_request| merge_request['updated_at'] } + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it 'returns an array of merge_requests ordered by created_at' do + path = endpoint_path + '?order_by=created_at&sort=asc' + + get api(path, user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + response_dates = json_response.map { |merge_request| merge_request['created_at'] } + expect(response_dates).to eq(response_dates.sort) + end + end + + context 'source_branch param' do + it 'returns merge requests with the given source branch' do + get api(endpoint_path, user), source_branch: merge_request_closed.source_branch, state: 'all' + + expect_response_contain_exactly(merge_request_closed, merge_request_merged) + end + end + + context 'target_branch param' do + it 'returns merge requests with the given target branch' do + get api(endpoint_path, user), target_branch: merge_request_closed.target_branch, state: 'all' + + expect_response_contain_exactly(merge_request_closed, merge_request_merged) + end + end + end +end diff --git a/spec/tasks/gitlab/storage_rake_spec.rb b/spec/tasks/gitlab/storage_rake_spec.rb index f59792c3d36..35e451b2f9a 100644 --- a/spec/tasks/gitlab/storage_rake_spec.rb +++ b/spec/tasks/gitlab/storage_rake_spec.rb @@ -1,13 +1,49 @@ require 'rake_helper' -describe 'gitlab:storage rake tasks' do +describe 'gitlab:storage:*' do before do Rake.application.rake_require 'tasks/gitlab/storage' stub_warn_user_is_not_gitlab end - describe 'migrate_to_hashed rake task' do + shared_examples "rake listing entities" do |entity_name, storage_type| + context 'limiting to 2' do + before do + stub_env('LIMIT' => 2) + end + + it "lists 2 out of 3 #{storage_type.downcase} #{entity_name}" do + create_collection + + expect { run_rake_task(task) }.to output(/Found 3 #{entity_name} using #{storage_type} Storage.*Displaying first 2 #{entity_name}/m).to_stdout + end + end + + context "without any #{storage_type.downcase} #{entity_name.singularize}" do + it 'displays message for empty results' do + expect { run_rake_task(task) }.to output(/Found 0 #{entity_name} using #{storage_type} Storage/).to_stdout + end + end + end + + shared_examples "rake entities summary" do |entity_name, storage_type| + context "with existing 3 #{storage_type.downcase} #{entity_name}" do + it "reports 3 #{storage_type.downcase} #{entity_name}" do + create_collection + + expect { run_rake_task(task) }.to output(/Found 3 #{entity_name} using #{storage_type} Storage/).to_stdout + end + end + + context "without any #{storage_type.downcase} #{entity_name.singularize}" do + it 'displays message for empty results' do + expect { run_rake_task(task) }.to output(/Found 0 #{entity_name} using #{storage_type} Storage/).to_stdout + end + end + end + + describe 'gitlab:storage:migrate_to_hashed' do context '0 legacy projects' do it 'does nothing' do expect(StorageMigratorWorker).not_to receive(:perform_async) @@ -16,8 +52,8 @@ describe 'gitlab:storage rake tasks' do end end - context '5 legacy projects' do - let(:projects) { create_list(:project, 5, storage_version: 0) } + context '3 legacy projects' do + let(:projects) { create_list(:project, 3, storage_version: 0) } context 'in batches of 1' do before do @@ -49,4 +85,64 @@ describe 'gitlab:storage rake tasks' do end end end + + describe 'gitlab:storage:legacy_projects' do + it_behaves_like 'rake entities summary', 'projects', 'Legacy' do + let(:task) { 'gitlab:storage:legacy_projects' } + let(:create_collection) { create_list(:project, 3, storage_version: 0) } + end + end + + describe 'gitlab:storage:list_legacy_projects' do + it_behaves_like 'rake listing entities', 'projects', 'Legacy' do + let(:task) { 'gitlab:storage:list_legacy_projects' } + let(:create_collection) { create_list(:project, 3, storage_version: 0) } + end + end + + describe 'gitlab:storage:hashed_projects' do + it_behaves_like 'rake entities summary', 'projects', 'Hashed' do + let(:task) { 'gitlab:storage:hashed_projects' } + let(:create_collection) { create_list(:project, 3, storage_version: 1) } + end + end + + describe 'gitlab:storage:list_hashed_projects' do + it_behaves_like 'rake listing entities', 'projects', 'Hashed' do + let(:task) { 'gitlab:storage:list_hashed_projects' } + let(:create_collection) { create_list(:project, 3, storage_version: 1) } + end + end + + describe 'gitlab:storage:legacy_attachments' do + it_behaves_like 'rake entities summary', 'attachments', 'Legacy' do + let(:task) { 'gitlab:storage:legacy_attachments' } + let(:project) { create(:project, storage_version: 1) } + let(:create_collection) { create_list(:upload, 3, model: project) } + end + end + + describe 'gitlab:storage:list_legacy_attachments' do + it_behaves_like 'rake listing entities', 'attachments', 'Legacy' do + let(:task) { 'gitlab:storage:list_legacy_attachments' } + let(:project) { create(:project, storage_version: 1) } + let(:create_collection) { create_list(:upload, 3, model: project) } + end + end + + describe 'gitlab:storage:hashed_attachments' do + it_behaves_like 'rake entities summary', 'attachments', 'Hashed' do + let(:task) { 'gitlab:storage:hashed_attachments' } + let(:project) { create(:project, storage_version: 2) } + let(:create_collection) { create_list(:upload, 3, model: project) } + end + end + + describe 'gitlab:storage:list_hashed_attachments' do + it_behaves_like 'rake listing entities', 'attachments', 'Hashed' do + let(:task) { 'gitlab:storage:list_hashed_attachments' } + let(:project) { create(:project, storage_version: 2) } + let(:create_collection) { create_list(:upload, 3, model: project) } + end + end end diff --git a/spec/tasks/tokens_spec.rb b/spec/tasks/tokens_spec.rb index 51f7a536cbb..555a58e9aa1 100644 --- a/spec/tasks/tokens_spec.rb +++ b/spec/tasks/tokens_spec.rb @@ -13,9 +13,9 @@ describe 'tokens rake tasks' do end end - describe 'reset_all_rss task' do + describe 'reset_all_feed task' do it 'invokes create_hooks task' do - expect { run_rake_task('tokens:reset_all_rss') }.to change { user.reload.rss_token } + expect { run_rake_task('tokens:reset_all_feed') }.to change { user.reload.feed_token } end end end diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb index 4165a005063..2dd0925a8e6 100644 --- a/spec/uploaders/object_storage_spec.rb +++ b/spec/uploaders/object_storage_spec.rb @@ -382,6 +382,8 @@ describe ObjectStorage do is_expected.to have_key(:RemoteObject) expect(subject[:RemoteObject]).to have_key(:ID) + expect(subject[:RemoteObject]).to include(Timeout: a_kind_of(Integer)) + expect(subject[:RemoteObject][:Timeout]).to be(ObjectStorage::DIRECT_UPLOAD_TIMEOUT) expect(subject[:RemoteObject]).to have_key(:GetURL) expect(subject[:RemoteObject]).to have_key(:DeleteURL) expect(subject[:RemoteObject]).to have_key(:StoreURL) diff --git a/spec/workers/concerns/waitable_worker_spec.rb b/spec/workers/concerns/waitable_worker_spec.rb index 54ab07981a4..199825b5097 100644 --- a/spec/workers/concerns/waitable_worker_spec.rb +++ b/spec/workers/concerns/waitable_worker_spec.rb @@ -7,9 +7,7 @@ describe WaitableWorker do 'Gitlab::Foo::Bar::DummyWorker' end - class << self - cattr_accessor(:counter) { 0 } - end + cattr_accessor(:counter) { 0 } include ApplicationWorker prepend WaitableWorker diff --git a/yarn.lock b/yarn.lock index b5ff26c4016..76d59b8ff52 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,53 +2,77 @@ # yarn lockfile v1 -"@babel/code-frame@7.0.0-beta.32", "@babel/code-frame@^7.0.0-beta.31": - version "7.0.0-beta.32" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.32.tgz#04f231b8ec70370df830d9926ce0f5add074ec4c" +"@babel/code-frame@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.44.tgz#2a02643368de80916162be70865c97774f3adbd9" dependencies: - chalk "^2.0.0" - esutils "^2.0.2" - js-tokens "^3.0.0" + "@babel/highlight" "7.0.0-beta.44" -"@babel/helper-function-name@7.0.0-beta.32": - version "7.0.0-beta.32" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.32.tgz#6161af4419f1b4e3ed2d28c0c79c160e218be1f3" +"@babel/generator@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.0.0-beta.44.tgz#c7e67b9b5284afcf69b309b50d7d37f3e5033d42" dependencies: - "@babel/helper-get-function-arity" "7.0.0-beta.32" - "@babel/template" "7.0.0-beta.32" - "@babel/types" "7.0.0-beta.32" + "@babel/types" "7.0.0-beta.44" + jsesc "^2.5.1" + lodash "^4.2.0" + source-map "^0.5.0" + trim-right "^1.0.1" -"@babel/helper-get-function-arity@7.0.0-beta.32": - version "7.0.0-beta.32" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.32.tgz#93721a99db3757de575a83bab7c453299abca568" +"@babel/helper-function-name@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.44.tgz#e18552aaae2231100a6e485e03854bc3532d44dd" dependencies: - "@babel/types" "7.0.0-beta.32" + "@babel/helper-get-function-arity" "7.0.0-beta.44" + "@babel/template" "7.0.0-beta.44" + "@babel/types" "7.0.0-beta.44" -"@babel/template@7.0.0-beta.32": - version "7.0.0-beta.32" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.32.tgz#e1d9fdbd2a7bcf128f2f920744a67dab18072495" +"@babel/helper-get-function-arity@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.44.tgz#d03ca6dd2b9f7b0b1e6b32c56c72836140db3a15" dependencies: - "@babel/code-frame" "7.0.0-beta.32" - "@babel/types" "7.0.0-beta.32" - babylon "7.0.0-beta.32" - lodash "^4.2.0" + "@babel/types" "7.0.0-beta.44" -"@babel/traverse@^7.0.0-beta.31": - version "7.0.0-beta.32" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.0.0-beta.32.tgz#b78b754c6e1af3360626183738e4c7a05951bc99" +"@babel/helper-split-export-declaration@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.44.tgz#c0b351735e0fbcb3822c8ad8db4e583b05ebd9dc" dependencies: - "@babel/code-frame" "7.0.0-beta.32" - "@babel/helper-function-name" "7.0.0-beta.32" - "@babel/types" "7.0.0-beta.32" - babylon "7.0.0-beta.32" - debug "^3.0.1" - globals "^10.0.0" + "@babel/types" "7.0.0-beta.44" + +"@babel/highlight@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0-beta.44.tgz#18c94ce543916a80553edcdcf681890b200747d5" + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^3.0.0" + +"@babel/template@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.44.tgz#f8832f4fdcee5d59bf515e595fc5106c529b394f" + dependencies: + "@babel/code-frame" "7.0.0-beta.44" + "@babel/types" "7.0.0-beta.44" + babylon "7.0.0-beta.44" + lodash "^4.2.0" + +"@babel/traverse@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.0.0-beta.44.tgz#a970a2c45477ad18017e2e465a0606feee0d2966" + dependencies: + "@babel/code-frame" "7.0.0-beta.44" + "@babel/generator" "7.0.0-beta.44" + "@babel/helper-function-name" "7.0.0-beta.44" + "@babel/helper-split-export-declaration" "7.0.0-beta.44" + "@babel/types" "7.0.0-beta.44" + babylon "7.0.0-beta.44" + debug "^3.1.0" + globals "^11.1.0" invariant "^2.2.0" lodash "^4.2.0" -"@babel/types@7.0.0-beta.32", "@babel/types@^7.0.0-beta.31": - version "7.0.0-beta.32" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.32.tgz#c317d0ecc89297b80bbcb2f50608e31f6452a5ff" +"@babel/types@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.44.tgz#6b1b164591f77dec0a0342aca995f2d046b3a757" dependencies: esutils "^2.0.2" lodash "^4.2.0" @@ -118,9 +142,9 @@ acorn@^3.0.4: version "3.3.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" -acorn@^5.0.0, acorn@^5.2.1, acorn@^5.3.0: - version "5.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.4.1.tgz#fdc58d9d17f4a4e98d102ded826a9b9759125102" +acorn@^5.0.0, acorn@^5.3.0, acorn@^5.5.0: + version "5.5.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9" addressparser@1.0.1: version "1.0.1" @@ -137,22 +161,22 @@ agent-base@2: extend "~3.0.0" semver "~5.0.1" -ajv-keywords@^1.0.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" +ajv-keywords@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762" ajv-keywords@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.1.0.tgz#ac2b27939c543e95d2c06e7f7f5c27be4aa543be" -ajv@^4.7.0, ajv@^4.9.1: +ajv@^4.9.1: version "4.11.8" resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" dependencies: co "^4.6.0" json-stable-stringify "^1.0.1" -ajv@^5.1.0: +ajv@^5.1.0, ajv@^5.2.3, ajv@^5.3.0: version "5.5.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" dependencies: @@ -201,7 +225,7 @@ ansi-align@^2.0.0: dependencies: string-width "^2.0.0" -ansi-escapes@^1.0.0, ansi-escapes@^1.1.0: +ansi-escapes@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" @@ -467,7 +491,7 @@ axios@^0.17.1: follow-redirects "^1.2.5" is-buffer "^1.1.5" -babel-code-frame@^6.16.0, babel-code-frame@^6.26.0: +babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" dependencies: @@ -499,14 +523,16 @@ babel-core@^6.26.0, babel-core@^6.26.3: slash "^1.0.0" source-map "^0.5.7" -babel-eslint@^8.0.2: - version "8.0.2" - resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.0.2.tgz#e44fb9a037d749486071d52d65312f5c20aa7530" +babel-eslint@^8.2.3: + version "8.2.3" + resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.2.3.tgz#1a2e6681cc9bc4473c32899e59915e19cd6733cf" dependencies: - "@babel/code-frame" "^7.0.0-beta.31" - "@babel/traverse" "^7.0.0-beta.31" - "@babel/types" "^7.0.0-beta.31" - babylon "^7.0.0-beta.31" + "@babel/code-frame" "7.0.0-beta.44" + "@babel/traverse" "7.0.0-beta.44" + "@babel/types" "7.0.0-beta.44" + babylon "7.0.0-beta.44" + eslint-scope "~3.7.1" + eslint-visitor-keys "^1.0.0" babel-generator@^6.18.0, babel-generator@^6.26.0: version "6.26.0" @@ -1105,18 +1131,14 @@ babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26 lodash "^4.17.4" to-fast-properties "^1.0.3" -babylon@7.0.0-beta.32, babylon@^7.0.0-beta.31: - version "7.0.0-beta.32" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.32.tgz#e9033cb077f64d6895f4125968b37dc0a8c3bc6e" +babylon@7.0.0-beta.44, babylon@^7.0.0-beta.30: + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.44.tgz#89159e15e6e30c5096e22d738d8c0af8a0e8ca1d" babylon@^6.17.3, babylon@^6.18.0: version "6.18.0" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" -babylon@^7.0.0-beta.30: - version "7.0.0-beta.44" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.44.tgz#89159e15e6e30c5096e22d738d8c0af8a0e8ca1d" - backo2@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" @@ -1378,11 +1400,11 @@ browserify-sign@^4.0.0: inherits "^2.0.1" parse-asn1 "^5.0.0" -browserify-zlib@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d" +browserify-zlib@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" dependencies: - pako "~0.2.0" + pako "~1.0.5" browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6: version "1.7.7" @@ -1391,6 +1413,10 @@ browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6: caniuse-db "^1.0.30000639" electron-to-chromium "^1.2.7" +buffer-from@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.0.0.tgz#4cb8832d23612589b0406e9e2956c17f06fdf531" + buffer-indexof@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.0.tgz#f54f647c4f4e25228baa656a2e57e43d5f270982" @@ -1423,7 +1449,7 @@ buildmail@4.0.1: nodemailer-shared "1.1.0" punycode "1.4.1" -builtin-modules@^1.0.0, builtin-modules@^1.1.1: +builtin-modules@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -1682,7 +1708,7 @@ cli-boxes@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" -cli-cursor@^1.0.1, cli-cursor@^1.0.2: +cli-cursor@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" dependencies: @@ -1906,10 +1932,11 @@ concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" -concat-stream@^1.5.0, concat-stream@^1.5.2: - version "1.6.0" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" +concat-stream@^1.5.0, concat-stream@^1.6.0: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" dependencies: + buffer-from "^1.0.0" inherits "^2.0.3" readable-stream "^2.2.2" typedarray "^0.0.6" @@ -2061,7 +2088,7 @@ cropper@^2.3.0: dependencies: jquery ">= 1.9.1" -cross-spawn@^5.0.1: +cross-spawn@^5.0.1, cross-spawn@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" dependencies: @@ -2314,18 +2341,6 @@ d3@3.5.17: version "3.5.17" resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.17.tgz#bc46748004378b21a360c9fc7cf5231790762fb8" -d@1: - version "1.0.0" - resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" - dependencies: - es5-ext "^0.10.9" - -d@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/d/-/d-0.1.1.tgz#da184c535d18d8ee7ba2aa229b914009fae11309" - dependencies: - es5-ext "~0.10.2" - dagre-d3-renderer@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/dagre-d3-renderer/-/dagre-d3-renderer-0.4.24.tgz#b36ce2fe4ea20de43e7698627c6ede2a9f15ec45" @@ -2376,18 +2391,12 @@ de-indent@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" -debug@2, debug@2.6.9, debug@^2.1.1, debug@^2.2.0, debug@^2.3.3, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9, debug@~2.6.4, debug@~2.6.6: +debug@2, debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9, debug@~2.6.4, debug@~2.6.6: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" dependencies: ms "2.0.0" -debug@2.2.0, debug@~2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" - dependencies: - ms "0.7.1" - debug@2.6.8: version "2.6.8" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" @@ -2400,6 +2409,12 @@ debug@^3.0.1, debug@^3.1.0, debug@~3.1.0: dependencies: ms "2.0.0" +debug@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" + dependencies: + ms "0.7.1" + decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -2593,12 +2608,11 @@ doctrine@1.5.0: esutils "^2.0.2" isarray "^1.0.0" -doctrine@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63" +doctrine@^2.0.2: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" dependencies: esutils "^2.0.2" - isarray "^1.0.0" document-register-element@1.3.0: version "1.3.0" @@ -2848,62 +2862,10 @@ es-to-primitive@^1.1.1: is-date-object "^1.0.1" is-symbol "^1.0.1" -es5-ext@^0.10.14, es5-ext@^0.10.8, es5-ext@^0.10.9, es5-ext@~0.10.14, es5-ext@~0.10.2: - version "0.10.24" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.24.tgz#a55877c9924bc0c8d9bd3c2cbe17495ac1709b14" - dependencies: - es6-iterator "2" - es6-symbol "~3.1" - -es6-iterator@2, es6-iterator@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.1.tgz#8e319c9f0453bf575d374940a655920e59ca5512" - dependencies: - d "1" - es5-ext "^0.10.14" - es6-symbol "^3.1" - -es6-map@^0.1.3: - version "0.1.5" - resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0" - dependencies: - d "1" - es5-ext "~0.10.14" - es6-iterator "~2.0.1" - es6-set "~0.1.5" - es6-symbol "~3.1.1" - event-emitter "~0.3.5" - es6-promise@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.0.2.tgz#010d5858423a5f118979665f46486a95c6ee2bb6" -es6-set@~0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1" - dependencies: - d "1" - es5-ext "~0.10.14" - es6-iterator "~2.0.1" - es6-symbol "3.1.1" - event-emitter "~0.3.5" - -es6-symbol@3, es6-symbol@3.1.1, es6-symbol@^3.1, es6-symbol@~3.1, es6-symbol@~3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" - dependencies: - d "1" - es5-ext "~0.10.14" - -es6-weak-map@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.1.tgz#0d2bbd8827eb5fb4ba8f97fbfea50d43db21ea81" - dependencies: - d "^0.1.1" - es5-ext "^0.10.8" - es6-iterator "2" - es6-symbol "3" - escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -2934,86 +2896,78 @@ escodegen@1.x.x: optionalDependencies: source-map "~0.5.6" -escope@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" +eslint-config-airbnb-base@^12.1.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-12.1.0.tgz#386441e54a12ccd957b0a92564a4bafebd747944" dependencies: - es6-map "^0.1.3" - es6-weak-map "^2.0.1" - esrecurse "^4.1.0" - estraverse "^4.1.1" - -eslint-config-airbnb-base@^10.0.1: - version "10.0.1" - resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-10.0.1.tgz#f17d4e52992c1d45d1b7713efbcd5ecd0e7e0506" + eslint-restricted-globals "^0.1.1" -eslint-import-resolver-node@^0.2.0: - version "0.2.3" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.2.3.tgz#5add8106e8c928db2cba232bcd9efa846e3da16c" +eslint-import-resolver-node@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a" dependencies: - debug "^2.2.0" - object-assign "^4.0.1" - resolve "^1.1.6" + debug "^2.6.9" + resolve "^1.5.0" -eslint-import-resolver-webpack@^0.8.3: - version "0.8.3" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.8.3.tgz#ad61e28df378a474459d953f246fd43f92675385" +eslint-import-resolver-webpack@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.10.0.tgz#b6f2468dc3e8b4ea076e5d75bece8da932789b07" dependencies: array-find "^1.0.0" debug "^2.6.8" enhanced-resolve "~0.9.0" - find-root "^0.1.1" + find-root "^1.1.0" has "^1.0.1" interpret "^1.0.0" - is-absolute "^0.2.3" - lodash.get "^3.7.0" - node-libs-browser "^1.0.0" - resolve "^1.2.0" + lodash "^4.17.4" + node-libs-browser "^1.0.0 || ^2.0.0" + resolve "^1.4.0" semver "^5.3.0" -eslint-module-utils@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.0.0.tgz#a6f8c21d901358759cdc35dbac1982ae1ee58bce" +eslint-module-utils@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.2.0.tgz#b270362cd88b1a48ad308976ce7fa54e98411746" dependencies: - debug "2.2.0" + debug "^2.6.8" pkg-dir "^1.0.0" -eslint-plugin-filenames@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-filenames/-/eslint-plugin-filenames-1.1.0.tgz#bb925218ab25b1aad1c622cfa9cb8f43cc03a4ff" +eslint-plugin-filenames@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-filenames/-/eslint-plugin-filenames-1.2.0.tgz#aee9c1c90189c95d2e49902c160eceefecd99f53" dependencies: - lodash.camelcase "4.1.1" - lodash.kebabcase "4.0.1" - lodash.snakecase "4.0.1" + lodash.camelcase "4.3.0" + lodash.kebabcase "4.1.1" + lodash.snakecase "4.1.1" + lodash.upperfirst "4.3.1" -eslint-plugin-html@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-html/-/eslint-plugin-html-2.0.1.tgz#3a829510e82522f1e2e44d55d7661a176121fce1" +eslint-plugin-html@4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-html/-/eslint-plugin-html-4.0.3.tgz#97d52dcf9e22724505d02719fbd02754013c8a17" dependencies: htmlparser2 "^3.8.2" -eslint-plugin-import@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.2.0.tgz#72ba306fad305d67c4816348a4699a4229ac8b4e" +eslint-plugin-import@^2.12.0: + version "2.12.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.12.0.tgz#dad31781292d6664b25317fd049d2e2b2f02205d" dependencies: - builtin-modules "^1.1.1" contains-path "^0.1.0" - debug "^2.2.0" + debug "^2.6.8" doctrine "1.5.0" - eslint-import-resolver-node "^0.2.0" - eslint-module-utils "^2.0.0" + eslint-import-resolver-node "^0.3.1" + eslint-module-utils "^2.2.0" has "^1.0.1" - lodash.cond "^4.3.0" + lodash "^4.17.4" minimatch "^3.0.3" - pkg-up "^1.0.0" + read-pkg-up "^2.0.0" + resolve "^1.6.0" eslint-plugin-jasmine@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/eslint-plugin-jasmine/-/eslint-plugin-jasmine-2.2.0.tgz#7135879383c39a667c721d302b9f20f0389543de" -eslint-plugin-promise@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-3.5.0.tgz#78fbb6ffe047201627569e85a6c5373af2a68fca" +eslint-plugin-promise@^3.8.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-3.8.0.tgz#65ebf27a845e3c1e9d6f6a5622ddd3801694b621" eslint-plugin-vue@^4.0.1: version "4.0.1" @@ -3022,7 +2976,11 @@ eslint-plugin-vue@^4.0.1: require-all "^2.2.0" vue-eslint-parser "^2.0.1" -eslint-scope@^3.7.1: +eslint-restricted-globals@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz#35f0d5cbc64c2e3ed62e93b4b1a7af05ba7ed4d7" + +eslint-scope@^3.7.1, eslint-scope@~3.7.1: version "3.7.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8" dependencies: @@ -3033,51 +2991,53 @@ eslint-visitor-keys@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" -eslint@^3.18.0: - version "3.19.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.19.0.tgz#c8fc6201c7f40dd08941b87c085767386a679acc" +eslint@~4.12.1: + version "4.12.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.12.1.tgz#5ec1973822b4a066b353770c3c6d69a2a188e880" dependencies: - babel-code-frame "^6.16.0" - chalk "^1.1.3" - concat-stream "^1.5.2" - debug "^2.1.1" - doctrine "^2.0.0" - escope "^3.6.0" - espree "^3.4.0" + ajv "^5.3.0" + babel-code-frame "^6.22.0" + chalk "^2.1.0" + concat-stream "^1.6.0" + cross-spawn "^5.1.0" + debug "^3.0.1" + doctrine "^2.0.2" + eslint-scope "^3.7.1" + espree "^3.5.2" esquery "^1.0.0" estraverse "^4.2.0" esutils "^2.0.2" file-entry-cache "^2.0.0" - glob "^7.0.3" - globals "^9.14.0" - ignore "^3.2.0" + functional-red-black-tree "^1.0.1" + glob "^7.1.2" + globals "^11.0.1" + ignore "^3.3.3" imurmurhash "^0.1.4" - inquirer "^0.12.0" - is-my-json-valid "^2.10.0" + inquirer "^3.0.6" is-resolvable "^1.0.0" - js-yaml "^3.5.1" - json-stable-stringify "^1.0.0" + js-yaml "^3.9.1" + json-stable-stringify-without-jsonify "^1.0.1" levn "^0.3.0" - lodash "^4.0.0" - mkdirp "^0.5.0" + lodash "^4.17.4" + minimatch "^3.0.2" + mkdirp "^0.5.1" natural-compare "^1.4.0" optionator "^0.8.2" - path-is-inside "^1.0.1" - pluralize "^1.2.1" - progress "^1.1.8" - require-uncached "^1.0.2" - shelljs "^0.7.5" - strip-bom "^3.0.0" + path-is-inside "^1.0.2" + pluralize "^7.0.0" + progress "^2.0.0" + require-uncached "^1.0.3" + semver "^5.3.0" + strip-ansi "^4.0.0" strip-json-comments "~2.0.1" - table "^3.7.8" + table "^4.0.1" text-table "~0.2.0" - user-home "^2.0.0" -espree@^3.4.0, espree@^3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.2.tgz#756ada8b979e9dcfcdb30aad8d1a9304a905e1ca" +espree@^3.5.2: + version "3.5.4" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7" dependencies: - acorn "^5.2.1" + acorn "^5.5.0" acorn-jsx "^3.0.0" esprima@2.7.x, esprima@^2.6.0, esprima@^2.7.1: @@ -3129,13 +3089,6 @@ eve-raphael@0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/eve-raphael/-/eve-raphael-0.5.0.tgz#17c754b792beef3fa6684d79cf5a47c63c4cda30" -event-emitter@~0.3.5: - version "0.3.5" - resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" - dependencies: - d "1" - es5-ext "~0.10.14" - event-stream@~3.3.0: version "3.3.4" resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571" @@ -3289,7 +3242,7 @@ extend@3, extend@^3.0.0, extend@~3.0.0, extend@~3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" -external-editor@^2.1.0: +external-editor@^2.0.4, external-editor@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5" dependencies: @@ -3362,7 +3315,7 @@ faye-websocket@~0.11.0: dependencies: websocket-driver ">=0.5.1" -figures@^1.3.5, figures@^1.7.0: +figures@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" dependencies: @@ -3447,9 +3400,9 @@ find-cache-dir@^1.0.0: make-dir "^1.0.0" pkg-dir "^2.0.0" -find-root@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/find-root/-/find-root-0.1.2.tgz#98d2267cff1916ccaf2743b3a0eea81d79d7dcd1" +find-root@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" find-up@^1.0.0: version "1.1.2" @@ -3627,6 +3580,10 @@ function-bind@^1.0.2, function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + fuzzaldrin-plus@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/fuzzaldrin-plus/-/fuzzaldrin-plus-0.5.0.tgz#ef5f26f0c2fc7e9e9a16ea149a802d6cb4804b1e" @@ -3776,11 +3733,11 @@ global-prefix@^1.0.1: is-windows "^1.0.1" which "^1.2.14" -globals@^10.0.0: - version "10.4.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-10.4.0.tgz#5c477388b128a9e4c5c5d01c7a2aca68c68b2da7" +globals@^11.0.1, globals@^11.1.0: + version "11.5.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.5.0.tgz#6bc840de6771173b191f13d3a9c94d441ee92642" -globals@^9.14.0, globals@^9.18.0: +globals@^9.18.0: version "9.18.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" @@ -4216,9 +4173,9 @@ httpreq@>=0.4.22: version "0.4.24" resolved "https://registry.yarnpkg.com/httpreq/-/httpreq-0.4.24.tgz#4335ffd82cd969668a39465c929ac61d6393627f" -https-browserify@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82" +https-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" https-proxy-agent@1: version "1.0.0" @@ -4258,9 +4215,9 @@ ignore-by-default@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" -ignore@^3.2.0, ignore@^3.3.5, ignore@^3.3.7: - version "3.3.7" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021" +ignore@^3.3.3, ignore@^3.3.5, ignore@^3.3.7: + version "3.3.8" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.8.tgz#3f8e9c35d38708a3a7e0e9abb6c73e7ee7707b2b" immediate@~3.0.5: version "3.0.6" @@ -4333,22 +4290,23 @@ ini@^1.3.4, ini@~1.3.0: version "1.3.5" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" -inquirer@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e" +inquirer@^3.0.6: + version "3.3.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9" dependencies: - ansi-escapes "^1.1.0" - ansi-regex "^2.0.0" - chalk "^1.0.0" - cli-cursor "^1.0.1" + ansi-escapes "^3.0.0" + chalk "^2.0.0" + cli-cursor "^2.1.0" cli-width "^2.0.0" - figures "^1.3.5" + external-editor "^2.0.4" + figures "^2.0.0" lodash "^4.3.0" - readline2 "^1.0.1" - run-async "^0.1.0" - rx-lite "^3.1.2" - string-width "^1.0.1" - strip-ansi "^3.0.0" + mute-stream "0.0.7" + run-async "^2.2.0" + rx-lite "^4.0.8" + rx-lite-aggregates "^4.0.8" + string-width "^2.1.0" + strip-ansi "^4.0.0" through "^2.3.6" inquirer@^5.1.0, inquirer@^5.2.0: @@ -4412,13 +4370,6 @@ is-absolute-url@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" -is-absolute@^0.2.3: - version "0.2.6" - resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-0.2.6.tgz#20de69f3db942ef2d87b9c2da36f172235b1b5eb" - dependencies: - is-relative "^0.2.1" - is-windows "^0.2.0" - is-accessor-descriptor@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" @@ -4560,7 +4511,7 @@ is-my-ip-valid@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824" -is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4: +is-my-json-valid@^2.12.4: version "2.17.2" resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz#6b2103a288e94ef3de5cf15d29dd85fc4b78d65c" dependencies: @@ -4666,12 +4617,6 @@ is-regex@^1.0.4: dependencies: has "^1.0.1" -is-relative@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-0.2.1.tgz#d27f4c7d516d175fb610db84bbeef23c3bc97aa5" - dependencies: - is-unc-path "^0.1.1" - is-resolvable@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.0.tgz#8df57c61ea2e3c501408d100fb013cf8d6e0cc62" @@ -4706,20 +4651,10 @@ is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" -is-unc-path@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-0.1.2.tgz#6ab053a72573c10250ff416a3814c35178af39b9" - dependencies: - unc-path-regex "^0.1.0" - is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" -is-windows@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c" - is-windows@^1.0.1, is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -4901,9 +4836,9 @@ js-tokens@^3.0.0, js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" -js-yaml@3.x, js-yaml@^3.5.1, js-yaml@^3.7.0: - version "3.9.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.9.1.tgz#08775cebdfdd359209f0d2acd383c8f86a6904a0" +js-yaml@3.x, js-yaml@^3.7.0, js-yaml@^3.9.1: + version "3.11.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef" dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -4963,6 +4898,10 @@ jsesc@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" +jsesc@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.1.tgz#e421a2a8e20d6b0819df28908f782526b96dd1fe" + jsesc@~0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" @@ -4983,7 +4922,11 @@ json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" -json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1: +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + +json-stable-stringify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" dependencies: @@ -5252,6 +5195,15 @@ load-json-file@^1.0.0: pinkie-promise "^2.0.0" strip-bom "^2.0.0" +load-json-file@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + strip-bom "^3.0.0" + load-json-file@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" @@ -5280,65 +5232,21 @@ locate-path@^2.0.0: p-locate "^2.0.0" path-exists "^3.0.0" -lodash._baseget@^3.0.0: - version "3.7.2" - resolved "https://registry.yarnpkg.com/lodash._baseget/-/lodash._baseget-3.7.2.tgz#1b6ae1d5facf3c25532350a13c1197cb8bb674f4" - -lodash._topath@^3.0.0: - version "3.8.1" - resolved "https://registry.yarnpkg.com/lodash._topath/-/lodash._topath-3.8.1.tgz#3ec5e2606014f4cb97f755fe6914edd8bfc00eac" - dependencies: - lodash.isarray "^3.0.0" - -lodash.camelcase@4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.1.1.tgz#065b3ff08f0b7662f389934c46a5504c90e0b2d8" - dependencies: - lodash.capitalize "^4.0.0" - lodash.deburr "^4.0.0" - lodash.words "^4.0.0" - -lodash.camelcase@^4.3.0: +lodash.camelcase@4.3.0, lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" -lodash.capitalize@^4.0.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz#f826c9b4e2a8511d84e3aca29db05e1a4f3b72a9" - lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" -lodash.cond@^4.3.0: - version "4.5.2" - resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5" - -lodash.deburr@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/lodash.deburr/-/lodash.deburr-4.1.0.tgz#ddb1bbb3ef07458c0177ba07de14422cb033ff9b" - lodash.escaperegexp@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" -lodash.get@^3.7.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-3.7.0.tgz#3ce68ae2c91683b281cc5394128303cbf75e691f" - dependencies: - lodash._baseget "^3.0.0" - lodash._topath "^3.0.0" - -lodash.isarray@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" - -lodash.kebabcase@4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.0.1.tgz#5e63bc9aa2a5562ff3b97ca7af2f803de1bcb90e" - dependencies: - lodash.deburr "^4.0.0" - lodash.words "^4.0.0" +lodash.kebabcase@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36" lodash.memoize@^4.1.2: version "4.1.2" @@ -5348,20 +5256,17 @@ lodash.mergewith@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz#150cf0a16791f5903b8891eab154609274bdea55" -lodash.snakecase@4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.0.1.tgz#bd012e5d2f93f7b58b9303e9a7fbfd5db13d6281" - dependencies: - lodash.deburr "^4.0.0" - lodash.words "^4.0.0" +lodash.snakecase@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d" lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" -lodash.words@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.words/-/lodash.words-4.2.0.tgz#5ecfeaf8ecf8acaa8e0c8386295f1993c9cf4036" +lodash.upperfirst@4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz#1365edf431480481ef0d1c68957a5ed99d49f7ce" lodash@4.17.4: version "4.17.4" @@ -5789,10 +5694,6 @@ multimatch@^2.0.0: arrify "^1.0.0" minimatch "^3.0.0" -mute-stream@0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" - mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" @@ -5846,57 +5747,29 @@ node-forge@0.6.33: version "0.6.33" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.6.33.tgz#463811879f573d45155ad6a9f43dc296e8e85ebc" -node-libs-browser@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-1.1.1.tgz#2a38243abedd7dffcd07a97c9aca5668975a6fea" - dependencies: - assert "^1.1.1" - browserify-zlib "^0.1.4" - buffer "^4.3.0" - console-browserify "^1.1.0" - constants-browserify "^1.0.0" - crypto-browserify "^3.11.0" - domain-browser "^1.1.1" - events "^1.0.0" - https-browserify "0.0.1" - os-browserify "^0.2.0" - path-browserify "0.0.0" - process "^0.11.0" - punycode "^1.2.4" - querystring-es3 "^0.2.0" - readable-stream "^2.0.5" - stream-browserify "^2.0.1" - stream-http "^2.3.1" - string_decoder "^0.10.25" - timers-browserify "^1.4.2" - tty-browserify "0.0.0" - url "^0.11.0" - util "^0.10.3" - vm-browserify "0.0.4" - -node-libs-browser@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.0.0.tgz#a3a59ec97024985b46e958379646f96c4b616646" +"node-libs-browser@^1.0.0 || ^2.0.0", node-libs-browser@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df" dependencies: assert "^1.1.1" - browserify-zlib "^0.1.4" + browserify-zlib "^0.2.0" buffer "^4.3.0" console-browserify "^1.1.0" constants-browserify "^1.0.0" crypto-browserify "^3.11.0" domain-browser "^1.1.1" events "^1.0.0" - https-browserify "0.0.1" - os-browserify "^0.2.0" + https-browserify "^1.0.0" + os-browserify "^0.3.0" path-browserify "0.0.0" - process "^0.11.0" + process "^0.11.10" punycode "^1.2.4" querystring-es3 "^0.2.0" - readable-stream "^2.0.5" + readable-stream "^2.3.3" stream-browserify "^2.0.1" - stream-http "^2.3.1" - string_decoder "^0.10.25" - timers-browserify "^2.0.2" + stream-http "^2.7.2" + string_decoder "^1.0.0" + timers-browserify "^2.0.4" tty-browserify "0.0.0" url "^0.11.0" util "^0.10.3" @@ -6191,9 +6064,9 @@ original@>=0.0.5: dependencies: url-parse "1.0.x" -os-browserify@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.2.1.tgz#63fc4ccee5d2d7763d26bbf8601078e6c2e0044f" +os-browserify@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" os-homedir@^1.0.0: version "1.0.2" @@ -6313,11 +6186,7 @@ package-json@^4.0.0: registry-url "^3.0.3" semver "^5.1.0" -pako@~0.2.0: - version "0.2.9" - resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" - -pako@~1.0.2: +pako@~1.0.2, pako@~1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258" @@ -6407,7 +6276,7 @@ path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" -path-is-inside@^1.0.1: +path-is-inside@^1.0.1, path-is-inside@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" @@ -6437,6 +6306,12 @@ path-type@^1.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" +path-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" + dependencies: + pify "^2.0.0" + path-type@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" @@ -6503,15 +6378,9 @@ pkg-dir@^2.0.0: dependencies: find-up "^2.1.0" -pkg-up@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-1.0.0.tgz#3e08fb461525c4421624a33b9f7e6d0af5b05a26" - dependencies: - find-up "^1.0.0" - -pluralize@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" +pluralize@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777" popper.js@^1.14.3: version "1.14.3" @@ -6829,13 +6698,13 @@ process-nextick-args@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" -process@^0.11.0, process@~0.11.0: +process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" -progress@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" +progress@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f" promise-inflight@^1.0.1: version "1.0.1" @@ -7043,6 +6912,13 @@ read-pkg-up@^1.0.1: find-up "^1.0.0" read-pkg "^1.0.0" +read-pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" + dependencies: + find-up "^2.0.0" + read-pkg "^2.0.0" + read-pkg-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" @@ -7058,6 +6934,14 @@ read-pkg@^1.0.0: normalize-package-data "^2.3.2" path-type "^1.0.0" +read-pkg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" + dependencies: + load-json-file "^2.0.0" + normalize-package-data "^2.3.2" + path-type "^2.0.0" + read-pkg@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" @@ -7066,7 +6950,7 @@ read-pkg@^3.0.0: normalize-package-data "^2.3.2" path-type "^3.0.0" -"readable-stream@1 || 2", readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.3: +"readable-stream@1 || 2", readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.3: version "2.3.4" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.4.tgz#c946c3f47fa7d8eabc0b6150f4a12f69a4574071" dependencies: @@ -7087,6 +6971,18 @@ readable-stream@1.1.x, "readable-stream@1.x >=1.1.9": isarray "0.0.1" string_decoder "~0.10.x" +readable-stream@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + readable-stream@~2.0.5, readable-stream@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" @@ -7107,14 +7003,6 @@ readdirp@^2.0.0: readable-stream "^2.0.2" set-immediate-shim "^1.0.1" -readline2@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - mute-stream "0.0.5" - recast@^0.12.5: version "0.12.9" resolved "https://registry.yarnpkg.com/recast/-/recast-0.12.9.tgz#e8e52bdb9691af462ccbd7c15d5a5113647a15f1" @@ -7376,7 +7264,7 @@ require-main-filename@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" -require-uncached@^1.0.2: +require-uncached@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" dependencies: @@ -7416,9 +7304,9 @@ resolve@1.1.x: version "1.1.7" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" -resolve@^1.1.6, resolve@^1.2.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36" +resolve@^1.1.6, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.6.0: + version "1.7.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3" dependencies: path-parse "^1.0.5" @@ -7469,12 +7357,6 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^2.0.0" inherits "^2.0.1" -run-async@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" - dependencies: - once "^1.3.0" - run-async@^2.0.0, run-async@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" @@ -7487,9 +7369,15 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" -rx-lite@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" +rx-lite-aggregates@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be" + dependencies: + rx-lite "*" + +rx-lite@*, rx-lite@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444" rxjs@^5.4.2, rxjs@^5.5.2: version "5.5.10" @@ -7677,14 +7565,6 @@ shebang-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" -shelljs@^0.7.5: - version "0.7.8" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3" - dependencies: - glob "^7.0.0" - interpret "^1.0.0" - rechoir "^0.6.2" - shelljs@^0.8.0: version "0.8.1" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.1.tgz#729e038c413a2254c4078b95ed46e0397154a9f1" @@ -7711,6 +7591,12 @@ slice-ansi@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" +slice-ansi@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d" + dependencies: + is-fullwidth-code-point "^2.0.0" + slide@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" @@ -7892,10 +7778,14 @@ source-map@^0.4.4: dependencies: amdefine ">=0.0.4" -source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.6: +source-map@^0.5.0, source-map@^0.5.7, source-map@~0.5.6: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" +source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1: + version "0.5.6" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" + source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" @@ -8029,13 +7919,13 @@ stream-each@^1.1.0: end-of-stream "^1.1.0" stream-shift "^1.0.0" -stream-http@^2.3.1: - version "2.8.0" - resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.0.tgz#fd86546dac9b1c91aff8fc5d287b98fafb41bc10" +stream-http@^2.7.2: + version "2.8.2" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.2.tgz#4126e8c6b107004465918aa2fc35549e77402c87" dependencies: builtin-status-codes "^3.0.0" inherits "^2.0.1" - readable-stream "^2.3.3" + readable-stream "^2.3.6" to-arraybuffer "^1.0.0" xtend "^4.0.0" @@ -8081,7 +7971,13 @@ string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -string_decoder@^0.10.25, string_decoder@~0.10.x: +string_decoder@^1.0.0, string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + dependencies: + safe-buffer "~5.1.0" + +string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" @@ -8189,16 +8085,16 @@ symbol-observable@^0.2.2: version "0.2.4" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-0.2.4.tgz#95a83db26186d6af7e7a18dbd9760a2f86d08f40" -table@^3.7.8: - version "3.8.3" - resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f" +table@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36" dependencies: - ajv "^4.7.0" - ajv-keywords "^1.0.0" - chalk "^1.1.1" - lodash "^4.0.0" - slice-ansi "0.0.4" - string-width "^2.0.0" + ajv "^5.2.3" + ajv-keywords "^2.1.0" + chalk "^2.1.0" + lodash "^4.17.4" + slice-ansi "1.0.0" + string-width "^2.1.1" tapable@^0.1.8: version "0.1.10" @@ -8301,15 +8197,9 @@ timed-out@^4.0.0, timed-out@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" -timers-browserify@^1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-1.4.2.tgz#c9c58b575be8407375cb5e2462dacee74359f41d" - dependencies: - process "~0.11.0" - -timers-browserify@^2.0.2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.4.tgz#96ca53f4b794a5e7c0e1bd7cc88a372298fa01e6" +timers-browserify@^2.0.4: + version "2.0.10" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.10.tgz#1d28e3d2aadf1d5a5996c4e9f95601cd053480ae" dependencies: setimmediate "^1.0.4" @@ -8477,10 +8367,6 @@ ultron@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c" -unc-path-regex@^0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" - undefsafe@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.2.tgz#225f6b9e0337663e0d8e7cfd686fc2836ccace76" @@ -8642,12 +8528,6 @@ use@^2.0.0: isobject "^3.0.0" lazy-cache "^2.0.2" -user-home@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f" - dependencies: - os-homedir "^1.0.0" - useragent@2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.2.1.tgz#cf593ef4f2d175875e8bb658ea92e18a4fd06d8e" @@ -8753,8 +8633,8 @@ void-elements@^2.0.0: resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" vue-eslint-parser@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-2.0.1.tgz#30135771c4fad00fdbac4542a2d59f3b1d776834" + version "2.0.3" + resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz#c268c96c6d94cfe3d938a5f7593959b0ca3360d1" dependencies: debug "^3.1.0" eslint-scope "^3.7.1" |