diff options
author | Filipa Lacerda <filipa@gitlab.com> | 2017-03-03 20:07:30 +0000 |
---|---|---|
committer | Filipa Lacerda <filipa@gitlab.com> | 2017-03-03 20:07:30 +0000 |
commit | 5d2c70b117600e3dd37e40bb206195e694a6546c (patch) | |
tree | 780308e54ad4cfb63a6fe4053139a2f969cf83eb | |
parent | 01f634d324da35e2a183bc8cfefc56bbda05a629 (diff) | |
parent | afd566347fe604d633adfebf0cd8277b860f52e0 (diff) | |
download | gitlab-ce-5d2c70b117600e3dd37e40bb206195e694a6546c.tar.gz |
Merge branch 'master' into widget-widget-widget-fun
* master: (163 commits)
Add KUBE_CA_PEM_FILE, deprecate KUBE_CA_PEM
Remove useless assignment
Rename query parameter to `membership`
Stop setting Strict-Transport-Securty header from within the app
Add filter param for authorized projects for current_user for V4
Improve EE compatibility MR docs [ci skip]
Fix the `Gitlab::Seeder` monkey patch to disable mail delivery
Use separate error class for cherry-pick and revert tree errors
Fix cherry-picking or reverting through an MR
Resolve "missing count badge styling in new sidebar dropdown"
Fix Rubocop offense in build retry service
Decrease tanuki logo size
remove extra whitespace on dashboard projects page
Changes after review
Delete hooks from project with empty repository
Only add a newline in Markdown Editor if the current line is not empty
Revert "Disallow system notes for closed issuables"
Cleaning up navigational order - Project
Backport EE changes. Service method should be `get` and not `all`
Remove bundle-audit ignores that are no longer applicable
...
624 files changed, 9596 insertions, 4500 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e7a279c828b..9006fef6a27 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -300,7 +300,7 @@ bundler:audit: - master@gitlab/gitlabhq - master@gitlab/gitlab-ee script: - - "bundle exec bundle-audit check --update --ignore OSVDB-115941 CVE-2016-6316 CVE-2016-6317" + - "bundle exec bundle-audit check --update" migration paths: stage: test diff --git a/.rubocop.yml b/.rubocop.yml index 38b71d74fea..fa1370ea1f3 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -23,6 +23,7 @@ AllCops: - 'tmp/**/*' - 'bin/**/*' - 'generator_templates/**/*' + - 'builds/**/*' # Gems in consecutive lines should be alphabetically sorted Bundler/OrderedGems: diff --git a/CHANGELOG.md b/CHANGELOG.md index c039335c46d..e075de055e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 8.17.2 (2017-03-01) + +- Expire all webpack assets after 8.17.1 included a badly compiled asset. !9602 + +## 8.17.1 (2017-02-28) + +- Replace setInterval with setTimeout to prevent highly frequent requests. !9271 (Takuya Noguchi) +- Disable unused tags count cache for Projects, Builds and Runners. +- Spam check and reCAPTCHA improvements. +- Allow searching issues for strings containing colons. +- Disabled tooltip on add issues button in usse boards. +- Fixed commit search UI. +- Fix MR changes tab size count when there are over 100 files in the diff. +- Disable invalid service templates. +- Use default branch as target_branch when parameter is missing. +- Upgrade GitLab Pages to v0.3.2. +- Add performance query regression fix for !9088 affecting #27267. +- Chat slash commands show labels correctly. + ## 8.17.0 (2017-02-22) - API: Fix file downloading. !0 (8267) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eed63127d9f..3cbc826e6db 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,32 +1,48 @@ +## Contributor license agreement + +By submitting code as an individual you agree to the +[individual contributor license agreement](doc/legal/individual_contributor_license_agreement.md). +By submitting code as an entity you agree to the +[corporate contributor license agreement](doc/legal/corporate_contributor_license_agreement.md). + +_This notice should stay as the first item in the CONTRIBUTING.MD file._ + +--- + <!-- START doctoc generated TOC please keep comment here to allow auto update --> <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* +- [Contributor license agreement](#contributor-license-agreement) - [Contribute to GitLab](#contribute-to-gitlab) - - [Contributor license agreement](#contributor-license-agreement) - - [Security vulnerability disclosure](#security-vulnerability-disclosure) - - [Closing policy for issues and merge requests](#closing-policy-for-issues-and-merge-requests) - - [Helping others](#helping-others) - - [I want to contribute!](#i-want-to-contribute) - - [Implement design & UI elements](#implement-design-ui-elements) - - [Issue tracker](#issue-tracker) - - [Feature proposals](#feature-proposals) - - [Issue tracker guidelines](#issue-tracker-guidelines) - - [Issue weight](#issue-weight) - - [Regression issues](#regression-issues) - - [Technical debt](#technical-debt) - - [Stewardship](#stewardship) - - [Merge requests](#merge-requests) - - [Merge request guidelines](#merge-request-guidelines) - - [Contribution acceptance criteria](#contribution-acceptance-criteria) - - [Changes for Stable Releases](#changes-for-stable-releases) - - [Definition of done](#definition-of-done) - - [Style guides](#style-guides) - - [Code of conduct](#code-of-conduct) +- [Security vulnerability disclosure](#security-vulnerability-disclosure) +- [Closing policy for issues and merge requests](#closing-policy-for-issues-and-merge-requests) +- [Helping others](#helping-others) +- [I want to contribute!](#i-want-to-contribute) +- [Implement design & UI elements](#implement-design-ui-elements) +- [Release retrospective and kickoff](#release-retrospective-and-kickoff) + - [Retrospective](#retrospective) + - [Kickoff](#kickoff) +- [Issue tracker](#issue-tracker) + - [Feature proposals](#feature-proposals) + - [Issue tracker guidelines](#issue-tracker-guidelines) + - [Issue weight](#issue-weight) + - [Regression issues](#regression-issues) + - [Technical debt](#technical-debt) + - [Stewardship](#stewardship) +- [Merge requests](#merge-requests) + - [Merge request guidelines](#merge-request-guidelines) + - [Contribution acceptance criteria](#contribution-acceptance-criteria) +- [Changes for Stable Releases](#changes-for-stable-releases) +- [Definition of done](#definition-of-done) +- [Style guides](#style-guides) +- [Code of conduct](#code-of-conduct) <!-- END doctoc generated TOC please keep comment here to allow auto update --> -# Contribute to GitLab +--- + +## Contribute to GitLab Thank you for your interest in contributing to GitLab. This guide details how to contribute to GitLab in a way that is efficient for everyone. @@ -41,13 +57,6 @@ operates please see [the GitLab contributing process](PROCESS.md). - [GitLab Inc engineers should refer to the engineering workflow document](https://about.gitlab.com/handbook/engineering/workflow/) -## Contributor license agreement - -By submitting code as an individual you agree to the -[individual contributor license agreement](doc/legal/individual_contributor_license_agreement.md). -By submitting code as an entity you agree to the -[corporate contributor license agreement](doc/legal/corporate_contributor_license_agreement.md). - ## Security vulnerability disclosure Please report suspected security vulnerabilities in private to @@ -68,7 +68,7 @@ gem 'gollum-rugged_adapter', '~> 0.4.2', require: false gem 'github-linguist', '~> 4.7.0', require: 'linguist' # API -gem 'grape', '~> 0.18.0' +gem 'grape', '~> 0.19.0' gem 'grape-entity', '~> 0.6.0' gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' @@ -236,7 +236,6 @@ gem 'gemojione', '~> 3.0' gem 'gon', '~> 6.1.0' gem 'jquery-atwho-rails', '~> 1.3.2' gem 'jquery-rails', '~> 4.1.0' -gem 'jquery-ui-rails', '~> 5.0.0' gem 'request_store', '~> 1.3' gem 'select2-rails', '~> 3.5.9' gem 'virtus', '~> 1.0.1' @@ -329,8 +328,6 @@ group :test do gem 'timecop', '~> 0.8.0' end -gem 'newrelic_rpm', '~> 3.16' - gem 'octokit', '~> 4.6.2' gem 'mail_room', '~> 0.9.1' @@ -352,3 +349,6 @@ gem 'health_check', '~> 2.2.0' # System information gem 'vmstat', '~> 2.3.0' gem 'sys-filesystem', '~> 1.1.6' + +# Gitaly GRPC client +gem 'gitaly', '~> 0.2.1' diff --git a/Gemfile.lock b/Gemfile.lock index 5ff18442c4f..2d7e6f6f0bf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -245,6 +245,9 @@ GEM json get_process_mem (0.2.0) gherkin-ruby (0.3.2) + gitaly (0.2.1) + google-protobuf (~> 3.1) + grpc (~> 1.0) github-linguist (4.7.6) charlock_holmes (~> 0.7.3) escape_utils (~> 1.1.0) @@ -296,6 +299,7 @@ GEM multi_json (~> 1.10) retriable (~> 1.4) signet (~> 0.6) + google-protobuf (3.2.0) googleauth (0.5.1) faraday (~> 0.9) jwt (~> 1.4) @@ -304,7 +308,7 @@ GEM multi_json (~> 1.11) os (~> 0.9) signet (~> 0.7) - grape (0.18.0) + grape (0.19.1) activesupport builder hashie (>= 2.1.0) @@ -317,6 +321,9 @@ GEM grape-entity (0.6.0) activesupport multi_json (>= 1.3.2) + grpc (1.1.2) + google-protobuf (~> 3.1) + googleauth (~> 0.5.1) haml (4.0.7) tilt haml_lint (0.21.0) @@ -353,8 +360,8 @@ GEM json (~> 1.8) multi_xml (>= 0.5.2) httpclient (2.8.2) - i18n (0.8.0) - ice_nine (0.11.1) + i18n (0.8.1) + ice_nine (0.11.2) influxdb (0.2.3) cause json @@ -367,8 +374,6 @@ GEM rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - jquery-ui-rails (5.0.5) - railties (>= 3.2.16) json (1.8.6) json-schema (2.6.2) addressable (~> 2.3.8) @@ -417,7 +422,7 @@ GEM minitest (5.7.0) mousetrap-rails (1.4.6) multi_json (1.12.1) - multi_xml (0.5.5) + multi_xml (0.6.0) multipart-post (2.0.0) mustermann (0.4.0) tool (~> 0.2) @@ -427,7 +432,6 @@ GEM net-ldap (0.12.1) net-ssh (3.0.1) netrc (0.11.0) - newrelic_rpm (3.16.0.318) nokogiri (1.6.8.1) mini_portile2 (~> 2.1.0) numerizer (0.1.1) @@ -660,7 +664,7 @@ GEM sexp_processor (~> 4.1) rubyntlm (0.5.2) rubypants (0.2.0) - rubyzip (1.2.0) + rubyzip (1.2.1) rufus-scheduler (3.1.10) rugged (0.24.0) safe_yaml (1.0.4) @@ -758,7 +762,7 @@ GEM eventmachine (~> 1.0, >= 1.0.4) rack (>= 1, < 3) thor (0.19.4) - thread_safe (0.3.5) + thread_safe (0.3.6) tilt (2.0.6) timecop (0.8.1) timfel-krb5-auth (0.8.3) @@ -878,6 +882,7 @@ DEPENDENCIES fuubar (~> 2.0.0) gemnasium-gitlab-service (~> 0.2) gemojione (~> 3.0) + gitaly (~> 0.2.1) github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.5.1) @@ -886,7 +891,7 @@ DEPENDENCIES gollum-rugged_adapter (~> 0.4.2) gon (~> 6.1.0) google-api-client (~> 0.8.6) - grape (~> 0.18.0) + grape (~> 0.19.0) grape-entity (~> 0.6.0) haml_lint (~> 0.21.0) hamlit (~> 2.6.1) @@ -899,7 +904,6 @@ DEPENDENCIES jira-ruby (~> 1.1.2) jquery-atwho-rails (~> 1.3.2) jquery-rails (~> 4.1.0) - jquery-ui-rails (~> 5.0.0) json-schema (~> 2.6.2) jwt (~> 1.5.6) kaminari (~> 0.17.0) @@ -915,7 +919,6 @@ DEPENDENCIES mousetrap-rails (~> 1.4.6) mysql2 (~> 0.3.16) net-ssh (~> 3.0.1) - newrelic_rpm (~> 3.16) nokogiri (~> 1.6.7, >= 1.6.7.2) oauth2 (~> 1.2.0) octokit (~> 4.6.2) @@ -1011,4 +1014,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.14.3 + 1.14.5 diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index c51860d1604..e0ee698a8ff 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -9,7 +9,6 @@ window.$ = window.jQuery = require('jquery'); require('jquery-ujs'); require('vendor/jquery.endless-scroll'); -require('vendor/jquery.highlight'); require('vendor/jquery.waitforimages'); require('vendor/jquery.caret'); require('vendor/jquery.atwho'); @@ -18,15 +17,11 @@ window.Cookies = require('js-cookie'); require('./autosave'); require('bootstrap/js/affix'); require('bootstrap/js/alert'); -require('bootstrap/js/button'); -require('bootstrap/js/collapse'); require('bootstrap/js/dropdown'); require('bootstrap/js/modal'); -require('bootstrap/js/scrollspy'); require('bootstrap/js/tab'); require('bootstrap/js/transition'); require('bootstrap/js/tooltip'); -require('bootstrap/js/popover'); require('select2/select2.js'); window.Pikaday = require('pikaday'); window._ = require('underscore'); @@ -236,6 +231,10 @@ require('es6-promise').polyfill(); var bootstrapBreakpoint = bp.getBreakpointSize(); var fitSidebarForSize; + $(document).on('scroll', function() { + $('.has-tooltip').tooltip('hide'); + }); + // Set the default path for all cookies to GitLab's root directory Cookies.defaults.path = gon.relative_url_root || '/'; diff --git a/app/assets/javascripts/boards/components/board_card.js b/app/assets/javascripts/boards/components/board_card.js index 52f61d84517..795b3cf2ec0 100644 --- a/app/assets/javascripts/boards/components/board_card.js +++ b/app/assets/javascripts/boards/components/board_card.js @@ -3,7 +3,7 @@ require('./issue_card_inner'); const Store = gl.issueBoards.BoardsStore; -module.exports = { +export default { name: 'BoardsIssueCard', template: ` <li class="card" diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6 index d92047cc0f8..2d52e96e7fb 100644 --- a/app/assets/javascripts/boards/components/board_list.js.es6 +++ b/app/assets/javascripts/boards/components/board_list.js.es6 @@ -2,8 +2,8 @@ /* global Vue */ /* global Sortable */ -const boardCard = require('./board_card'); -require('./board_new_issue'); +import boardNewIssue from './board_new_issue'; +import boardCard from './board_card'; (() => { const Store = gl.issueBoards.BoardsStore; @@ -15,7 +15,7 @@ require('./board_new_issue'); template: '#js-board-list-template', components: { boardCard, - 'board-new-issue': gl.issueBoards.BoardNewIssue + boardNewIssue, }, props: { disabled: Boolean, @@ -81,6 +81,12 @@ require('./board_new_issue'); }); } }, + toggleForm() { + this.showIssueForm = !this.showIssueForm; + }, + }, + created() { + gl.IssueBoardsApp.$on(`hide-issue-form-${this.list.id}`, this.toggleForm); }, mounted () { const options = gl.issueBoards.getBoardSortableDefaultOptions({ @@ -115,6 +121,9 @@ require('./board_new_issue'); this.loadNextPage(); } }; - } + }, + beforeDestroy() { + gl.IssueBoardsApp.$off(`hide-issue-form-${this.list.id}`, this.toggleForm); + }, }); })(); diff --git a/app/assets/javascripts/boards/components/board_new_issue.js b/app/assets/javascripts/boards/components/board_new_issue.js new file mode 100644 index 00000000000..b88f59dd6d4 --- /dev/null +++ b/app/assets/javascripts/boards/components/board_new_issue.js @@ -0,0 +1,92 @@ +/* global ListIssue */ +const Store = gl.issueBoards.BoardsStore; + +export default { + name: 'BoardNewIssue', + props: { + list: Object, + }, + data() { + return { + title: '', + error: false, + }; + }, + methods: { + submit(e) { + e.preventDefault(); + if (this.title.trim() === '') return; + + this.error = false; + + const labels = this.list.label ? [this.list.label] : []; + const issue = new ListIssue({ + title: this.title, + labels, + subscribed: true, + }); + + this.list.newIssue(issue) + .then(() => { + // Need this because our jQuery very kindly disables buttons on ALL form submissions + $(this.$refs.submitButton).enable(); + + Store.detail.issue = issue; + Store.detail.list = this.list; + }) + .catch(() => { + // Need this because our jQuery very kindly disables buttons on ALL form submissions + $(this.$refs.submitButton).enable(); + + // Remove the issue + this.list.removeIssue(issue); + + // Show error message + this.error = true; + }); + + this.cancel(); + }, + cancel() { + this.title = ''; + gl.IssueBoardsApp.$emit(`hide-issue-form-${this.list.id}`); + }, + }, + mounted() { + this.$refs.input.focus(); + }, + template: ` + <div class="card board-new-issue-form"> + <form @submit="submit($event)"> + <div class="flash-container" + v-if="error"> + <div class="flash-alert"> + An error occured. Please try again. + </div> + </div> + <label class="label-light" + :for="list.id + '-title'"> + Title + </label> + <input class="form-control" + type="text" + v-model="title" + ref="input" + :id="list.id + '-title'" /> + <div class="clearfix prepend-top-10"> + <button class="btn btn-success pull-left" + type="submit" + :disabled="title === ''" + ref="submit-button"> + Submit issue + </button> + <button class="btn btn-default pull-right" + type="button" + @click="cancel"> + Cancel + </button> + </div> + </form> + </div> + `, +}; diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6 deleted file mode 100644 index b5c14a198ba..00000000000 --- a/app/assets/javascripts/boards/components/board_new_issue.js.es6 +++ /dev/null @@ -1,64 +0,0 @@ -/* eslint-disable comma-dangle, no-unused-vars */ -/* global Vue */ -/* global ListIssue */ - -(() => { - const Store = gl.issueBoards.BoardsStore; - - window.gl = window.gl || {}; - - gl.issueBoards.BoardNewIssue = Vue.extend({ - props: { - list: Object, - }, - data() { - return { - title: '', - error: false - }; - }, - methods: { - submit(e) { - e.preventDefault(); - if (this.title.trim() === '') return; - - this.error = false; - - const labels = this.list.label ? [this.list.label] : []; - const issue = new ListIssue({ - title: this.title, - labels, - subscribed: true - }); - - this.list.newIssue(issue) - .then((data) => { - // Need this because our jQuery very kindly disables buttons on ALL form submissions - $(this.$refs.submitButton).enable(); - - Store.detail.issue = issue; - Store.detail.list = this.list; - }) - .catch(() => { - // Need this because our jQuery very kindly disables buttons on ALL form submissions - $(this.$refs.submitButton).enable(); - - // Remove the issue - this.list.removeIssue(issue); - - // Show error message - this.error = true; - }); - - this.cancel(); - }, - cancel() { - this.title = ''; - this.$parent.showIssueForm = false; - } - }, - mounted() { - this.$refs.input.focus(); - }, - }); -})(); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 index e7c6c063413..631ed34851c 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 @@ -39,15 +39,10 @@ const PipelineStore = require('./pipelines_store'); */ data() { const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset; - const svgsData = document.querySelector('.pipeline-svgs').dataset; const store = new PipelineStore(); - // Transform svgs DOMStringMap to a plain Object. - const svgsObject = gl.utils.DOMStringMapToObject(svgsData); - return { endpoint: pipelinesTableData.endpoint, - svgs: svgsObject, store, state: store.state, isLoading: false, @@ -69,7 +64,9 @@ const PipelineStore = require('./pipelines_store'); return pipelinesService.all() .then(response => response.json()) .then((json) => { - this.store.storePipelines(json); + // depending of the endpoint the response can either bring a `pipelines` key or not. + const pipelines = json.pipelines || json; + this.store.storePipelines(pipelines); this.isLoading = false; }) .catch(() => { @@ -99,10 +96,7 @@ const PipelineStore = require('./pipelines_store'); <div class="table-holder pipelines" v-if="!isLoading && state.pipelines.length > 0"> - <pipelines-table-component - :pipelines="state.pipelines" - :svgs="svgs"> - </pipelines-table-component> + <pipelines-table-component :pipelines="state.pipelines"/> </div> </div> `, diff --git a/app/assets/javascripts/copy_as_gfm.js.es6 b/app/assets/javascripts/copy_as_gfm.js.es6 index 4bd537a6f28..2bc3d85fba4 100644 --- a/app/assets/javascripts/copy_as_gfm.js.es6 +++ b/app/assets/javascripts/copy_as_gfm.js.es6 @@ -25,6 +25,9 @@ require('./lib/utils/common_utils'); }, }, ReferenceFilter: { + '.tooltip'(el, text) { + return ''; + }, 'a.gfm:not([data-link=true])'(el, text) { return el.dataset.original || text; }, diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 index 8652479e7bf..42e1bbce744 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign */ -/* global Vue */ +import Vue from 'vue'; +import iconCommit from '../svg/icon_commit.svg'; ((global) => { global.cycleAnalytics = global.cycleAnalytics || {}; @@ -9,6 +10,11 @@ items: Array, stage: Object, }, + + data() { + return { iconCommit }; + }, + template: ` <div> <div class="events-description"> @@ -31,7 +37,7 @@ </h5> <span> First - <span class="commit-icon">${global.cycleAnalytics.svgs.iconCommit}</span> + <span class="commit-icon">${iconCommit}</span> <a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a> pushed by <a :href="commit.author.webUrl" class="commit-author-link"> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6 index 82622232f64..8fa63734cf1 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6 @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign */ -/* global Vue */ +import Vue from 'vue'; +import iconBranch from '../svg/icon_branch.svg'; ((global) => { global.cycleAnalytics = global.cycleAnalytics || {}; @@ -9,6 +10,9 @@ items: Array, stage: Object, }, + data() { + return { iconBranch }; + }, template: ` <div> <div class="events-description"> @@ -22,7 +26,7 @@ <a :href="build.url" class="pipeline-id">#{{ build.id }}</a> <i class="fa fa-code-fork"></i> <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a> - <span class="icon-branch">${global.cycleAnalytics.svgs.iconBranch}</span> + <span class="icon-branch">${iconBranch}</span> <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a> </h5> <span> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 index 4bfd363a1f1..0015249cfaa 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 @@ -1,5 +1,7 @@ /* eslint-disable no-param-reassign */ -/* global Vue */ +import Vue from 'vue'; +import iconBuildStatus from '../svg/icon_build_status.svg'; +import iconBranch from '../svg/icon_branch.svg'; ((global) => { global.cycleAnalytics = global.cycleAnalytics || {}; @@ -9,6 +11,9 @@ items: Array, stage: Object, }, + data() { + return { iconBuildStatus, iconBranch }; + }, template: ` <div> <div class="events-description"> @@ -18,13 +23,13 @@ <li v-for="build in items" class="stage-event-item item-build-component"> <div class="item-details"> <h5 class="item-title"> - <span class="icon-build-status">${global.cycleAnalytics.svgs.iconBuildStatus}</span> + <span class="icon-build-status">${iconBuildStatus}</span> <a :href="build.url" class="item-build-name">{{ build.name }}</a> · <a :href="build.url" class="pipeline-id">#{{ build.id }}</a> <i class="fa fa-code-fork"></i> <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a> - <span class="icon-branch">${global.cycleAnalytics.svgs.iconBranch}</span> + <span class="icon-branch">${iconBranch}</span> <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a> </h5> <span> diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 index 411ac7b24b2..beff293b587 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 @@ -4,9 +4,6 @@ window.Vue = require('vue'); window.Cookies = require('js-cookie'); -require('./svg/icon_branch'); -require('./svg/icon_build_status'); -require('./svg/icon_commit'); require('./components/stage_code_component'); require('./components/stage_issue_component'); require('./components/stage_plan_component'); diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 index 3efeb141008..7ae9de7297c 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 @@ -75,8 +75,11 @@ const DEFAULT_EVENT_OBJECTS = require('./default_event_objects'); const eventItem = Object.assign({}, DEFAULT_EVENT_OBJECTS[stage.slug], item); eventItem.totalTime = eventItem.total_time; - eventItem.author.webUrl = eventItem.author.web_url; - eventItem.author.avatarUrl = eventItem.author.avatar_url; + + if (eventItem.author) { + eventItem.author.webUrl = eventItem.author.web_url; + eventItem.author.avatarUrl = eventItem.author.avatar_url; + } if (eventItem.created_at) eventItem.createdAt = eventItem.created_at; if (eventItem.short_sha) eventItem.shortSha = eventItem.short_sha; diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6 b/app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6 deleted file mode 100644 index 5d486bcaf66..00000000000 --- a/app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6 +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-disable no-param-reassign */ -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; - global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {}; - - global.cycleAnalytics.svgs.iconBranch = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><path fill="#8C8C8C" fill-rule="evenodd" d="M9.678 6.722C9.353 5.167 8.053 4 6.5 4S3.647 5.167 3.322 6.722h-2.6c-.397 0-.722.35-.722.778 0 .428.325.778.722.778h2.6C3.647 9.833 4.947 11 6.5 11s2.853-1.167 3.178-2.722h2.6c.397 0 .722-.35.722-.778 0-.428-.325-.778-.722-.778h-2.6zM4.694 7.5c0-1.09.795-1.944 1.806-1.944 1.01 0 1.806.855 1.806 1.944 0 1.09-.795 1.944-1.806 1.944-1.01 0-1.806-.855-1.806-1.944z"/></svg>'; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_branch.svg b/app/assets/javascripts/cycle_analytics/svg/icon_branch.svg new file mode 100644 index 00000000000..9f547d3d744 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/svg/icon_branch.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><path fill="#8C8C8C" fill-rule="evenodd" d="M9.678 6.722C9.353 5.167 8.053 4 6.5 4S3.647 5.167 3.322 6.722h-2.6c-.397 0-.722.35-.722.778 0 .428.325.778.722.778h2.6C3.647 9.833 4.947 11 6.5 11s2.853-1.167 3.178-2.722h2.6c.397 0 .722-.35.722-.778 0-.428-.325-.778-.722-.778h-2.6zM4.694 7.5c0-1.09.795-1.944 1.806-1.944 1.01 0 1.806.855 1.806 1.944 0 1.09-.795 1.944-1.806 1.944-1.01 0-1.806-.855-1.806-1.944z"/></svg> diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6 b/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6 deleted file mode 100644 index 661bf9e9f1c..00000000000 --- a/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6 +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-disable no-param-reassign */ -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; - global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {}; - - global.cycleAnalytics.svgs.iconBuildStatus = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><g fill="#31AF64" fill-rule="evenodd"><path d="M12.5 7c0-3.038-2.462-5.5-5.5-5.5S1.5 3.962 1.5 7s2.462 5.5 5.5 5.5 5.5-2.462 5.5-5.5zM0 7c0-3.866 3.134-7 7-7s7 3.134 7 7-3.134 7-7 7-7-3.134-7-7z"/><path d="M6.28 7.697L5.045 6.464c-.117-.117-.305-.117-.42-.002l-.614.614c-.11.113-.11.303.007.42l1.91 1.91c.19.19.51.197.703.004l.264-.265L9.997 6.04c.108-.107.107-.293-.01-.408l-.612-.614c-.114-.113-.298-.12-.41-.01L6.28 7.7z"/></g></svg>'; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_build_status.svg b/app/assets/javascripts/cycle_analytics/svg/icon_build_status.svg new file mode 100644 index 00000000000..b932d90618a --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/svg/icon_build_status.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><g fill="#31AF64" fill-rule="evenodd"><path d="M12.5 7c0-3.038-2.462-5.5-5.5-5.5S1.5 3.962 1.5 7s2.462 5.5 5.5 5.5 5.5-2.462 5.5-5.5zM0 7c0-3.866 3.134-7 7-7s7 3.134 7 7-3.134 7-7 7-7-3.134-7-7z"/><path d="M6.28 7.697L5.045 6.464c-.117-.117-.305-.117-.42-.002l-.614.614c-.11.113-.11.303.007.42l1.91 1.91c.19.19.51.197.703.004l.264-.265L9.997 6.04c.108-.107.107-.293-.01-.408l-.612-.614c-.114-.113-.298-.12-.41-.01L6.28 7.7z"/></g></svg> diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6 b/app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6 deleted file mode 100644 index 2208c27a619..00000000000 --- a/app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6 +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-disable no-param-reassign */ -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; - global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {}; - - global.cycleAnalytics.svgs.iconCommit = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><path fill="#8F8F8F" fill-rule="evenodd" d="M28.777 18c-.91-4.008-4.494-7-8.777-7-4.283 0-7.868 2.992-8.777 7H4.01C2.9 18 2 18.895 2 20c0 1.112.9 2 2.01 2h7.213c.91 4.008 4.494 7 8.777 7 4.283 0 7.868-2.992 8.777-7h7.214C37.1 22 38 21.105 38 20c0-1.112-.9-2-2.01-2h-7.213zM20 25c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5z"/></svg>'; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_commit.svg b/app/assets/javascripts/cycle_analytics/svg/icon_commit.svg new file mode 100644 index 00000000000..6a517756058 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/svg/icon_commit.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><path fill="#8F8F8F" fill-rule="evenodd" d="M28.777 18c-.91-4.008-4.494-7-8.777-7-4.283 0-7.868 2.992-8.777 7H4.01C2.9 18 2 18.895 2 20c0 1.112.9 2 2.01 2h7.213c.91 4.008 4.494 7 8.777 7 4.283 0 7.868-2.992 8.777-7h7.214C37.1 22 38 21.105 38 20c0-1.112-.9-2-2.01-2h-7.213zM20 25c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5z"/></svg> diff --git a/app/assets/javascripts/diff.js.es6 b/app/assets/javascripts/diff.js.es6 index ccccd0a36ff..6829e8aeaea 100644 --- a/app/assets/javascripts/diff.js.es6 +++ b/app/assets/javascripts/diff.js.es6 @@ -25,6 +25,10 @@ require('./lib/utils/url_utility'); isBound = true; } + if (gl.utils.getLocationHash()) { + this.highlightSelectedLine(); + } + this.openAnchoredDiff(); } @@ -78,7 +82,7 @@ require('./lib/utils/url_utility'); if (nothingHereBlock.length) { const clickTarget = $('.js-file-title, .click-to-expand', diffFile); diffFile.data('singleFileDiff').toggleDiff(clickTarget, () => { - this.highlighSelectedLine(); + this.highlightSelectedLine(); if (cb) cb(); }); } else if (cb) { @@ -94,7 +98,7 @@ require('./lib/utils/url_utility'); } else { window.location.hash = hash; } - this.highlighSelectedLine(); + this.highlightSelectedLine(); } diffViewType() { @@ -108,7 +112,7 @@ require('./lib/utils/url_utility'); return line.find('.diff-line-num').map((i, elm) => parseInt($(elm).data('linenumber'), 10)); } - highlighSelectedLine() { + highlightSelectedLine() { const hash = gl.utils.getLocationHash(); const $diffFiles = $('.diff-file'); $diffFiles.find('.hll').removeClass('hll'); diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 0f678492d4c..fc25122aedc 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -35,6 +35,8 @@ /* global Labels */ /* global Shortcuts */ +import GroupsList from './groups_list'; + const ShortcutsBlob = require('./shortcuts_blob'); const UserCallout = require('./user_callout'); @@ -96,6 +98,10 @@ const UserCallout = require('./user_callout'); case 'dashboard:todos:index': new gl.Todos(); break; + case 'dashboard:groups:index': + case 'explore:groups:index': + new GroupsList(); + break; case 'projects:milestones:new': case 'projects:milestones:edit': case 'projects:milestones:update': diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index 4b700a39d44..2cb48dde628 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -35,9 +35,6 @@ module.exports = Vue.component('environment-component', { projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath, newEnvironmentPath: environmentsData.newEnvironmentPath, helpPagePath: environmentsData.helpPagePath, - commitIconSvg: environmentsData.commitIconSvg, - playIconSvg: environmentsData.playIconSvg, - terminalIconSvg: environmentsData.terminalIconSvg, // Pagination Properties, paginationInformation: {}, @@ -78,7 +75,7 @@ module.exports = Vue.component('environment-component', { this.isLoading = true; - return service.all() + return service.get() .then(resp => ({ headers: resp.headers, body: resp.json(), @@ -145,7 +142,7 @@ module.exports = Vue.component('environment-component', { </div> </div> - <div class="environments-container"> + <div class="content-list environments-container"> <div class="environments-list-loading text-center" v-if="isLoading"> <i class="fa fa-spinner fa-spin"></i> </div> @@ -176,17 +173,13 @@ module.exports = Vue.component('environment-component', { <environment-table :environments="state.environments" :can-create-deployment="canCreateDeploymentParsed" - :can-read-environment="canReadEnvironmentParsed" - :play-icon-svg="playIconSvg" - :terminal-icon-svg="terminalIconSvg" - :commit-icon-svg="commitIconSvg"> - </environment-table> - - <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1" - :change="changePage" - :pageInfo="state.paginationInformation"> - </table-pagination> + :can-read-environment="canReadEnvironmentParsed"/> </div> + + <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1" + :change="changePage" + :pageInfo="state.paginationInformation"> + </table-pagination> </div> </div> `, diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6 index 978d4dd8b6b..15e3f8823d2 100644 --- a/app/assets/javascripts/environments/components/environment_actions.js.es6 +++ b/app/assets/javascripts/environments/components/environment_actions.js.es6 @@ -1,4 +1,5 @@ const Vue = require('vue'); +const playIconSvg = require('icons/_icon_play.svg'); module.exports = Vue.component('actions-component', { props: { @@ -7,11 +8,10 @@ module.exports = Vue.component('actions-component', { required: false, default: () => [], }, + }, - playIconSvg: { - type: String, - required: false, - }, + data() { + return { playIconSvg }; }, template: ` @@ -28,9 +28,7 @@ module.exports = Vue.component('actions-component', { data-method="post" rel="nofollow" class="js-manual-action-link"> - - <span class="js-action-play-icon-container" v-html="playIconSvg"></span> - + ${playIconSvg} <span> {{action.name}} </span> diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 5d780bddb0e..7f4e070b229 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -46,21 +46,6 @@ module.exports = Vue.component('environment-item', { required: false, default: false, }, - - commitIconSvg: { - type: String, - required: false, - }, - - playIconSvg: { - type: String, - required: false, - }, - - terminalIconSvg: { - type: String, - required: false, - }, }, computed: { @@ -487,9 +472,7 @@ module.exports = Vue.component('environment-item', { :commit-url="commitUrl" :short-sha="commitShortSha" :title="commitTitle" - :author="commitAuthor" - :commit-icon-svg="commitIconSvg"> - </commit-component> + :author="commitAuthor"/> </div> <p v-if="!model.isFolder && !hasLastDeploymentKey" class="commit-title"> No deployments yet @@ -503,30 +486,23 @@ module.exports = Vue.component('environment-item', { </span> </td> - <td class="hidden-xs environments-actions"> + <td class="environments-actions"> <div v-if="!model.isFolder" class="btn-group pull-right" role="group"> <actions-component v-if="hasManualActions && canCreateDeployment" - :play-icon-svg="playIconSvg" - :actions="manualActions"> - </actions-component> + :actions="manualActions"/> <external-url-component v-if="externalURL && canReadEnvironment" - :external-url="externalURL"> - </external-url-component> + :external-url="externalURL"/> <stop-component v-if="hasStopAction && canCreateDeployment" - :stop-url="model.stop_path"> - </stop-component> + :stop-url="model.stop_path"/> <terminal-button-component v-if="model && model.terminal_path" - :terminal-icon-svg="terminalIconSvg" - :terminal-path="model.terminal_path"> - </terminal-button-component> + :terminal-path="model.terminal_path"/> <rollback-component v-if="canRetry && canCreateDeployment" :is-last-deployment="isLastDeployment" - :retry-url="retryUrl"> - </rollback-component> + :retry-url="retryUrl"/> </div> </td> </tr> diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 index 481e0d15e7a..e86607e78f4 100644 --- a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 +++ b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 @@ -3,6 +3,7 @@ * Used in environments table. */ const Vue = require('vue'); +const terminalIconSvg = require('icons/_icon_terminal.svg'); module.exports = Vue.component('terminal-button-component', { props: { @@ -10,16 +11,16 @@ module.exports = Vue.component('terminal-button-component', { type: String, default: '', }, - terminalIconSvg: { - type: String, - default: '', - }, + }, + + data() { + return { terminalIconSvg }; }, template: ` <a class="btn terminal-button" :href="terminalPath"> - <span class="js-terminal-icon-container" v-html="terminalIconSvg"></span> + ${terminalIconSvg} </a> `, }); diff --git a/app/assets/javascripts/environments/components/environments_table.js.es6 b/app/assets/javascripts/environments/components/environments_table.js.es6 index fd35d77fd3d..4088d63be80 100644 --- a/app/assets/javascripts/environments/components/environments_table.js.es6 +++ b/app/assets/javascripts/environments/components/environments_table.js.es6 @@ -28,25 +28,10 @@ module.exports = Vue.component('environment-table-component', { required: false, default: false, }, - - commitIconSvg: { - type: String, - required: false, - }, - - playIconSvg: { - type: String, - required: false, - }, - - terminalIconSvg: { - type: String, - required: false, - }, }, template: ` - <table class="table ci-table environments"> + <table class="table ci-table"> <thead> <tr> <th class="environments-name">Environment</th> @@ -54,7 +39,7 @@ module.exports = Vue.component('environment-table-component', { <th class="environments-build">Job</th> <th class="environments-commit">Commit</th> <th class="environments-date">Updated</th> - <th class="hidden-xs environments-actions"></th> + <th class="environments-actions"></th> </tr> </thead> <tbody> @@ -63,10 +48,7 @@ module.exports = Vue.component('environment-table-component', { <tr is="environment-item" :model="model" :can-create-deployment="canCreateDeployment" - :can-read-environment="canReadEnvironment" - :play-icon-svg="playIconSvg" - :terminal-icon-svg="terminalIconSvg" - :commit-icon-svg="commitIconSvg"></tr> + :can-read-environment="canReadEnvironment"></tr> </template> </tbody> </table> diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.js.es6 b/app/assets/javascripts/environments/folder/environments_folder_view.js.es6 index 53d52965758..2a9d0492d7a 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_view.js.es6 +++ b/app/assets/javascripts/environments/folder/environments_folder_view.js.es6 @@ -92,7 +92,7 @@ module.exports = Vue.component('environment-folder-view', { this.isLoading = true; - return service.all() + return service.get() .then(resp => ({ headers: resp.headers, body: resp.json(), diff --git a/app/assets/javascripts/environments/services/environments_service.js.es6 b/app/assets/javascripts/environments/services/environments_service.js.es6 index 9cef335868e..effc6c4c838 100644 --- a/app/assets/javascripts/environments/services/environments_service.js.es6 +++ b/app/assets/javascripts/environments/services/environments_service.js.es6 @@ -5,7 +5,7 @@ class EnvironmentsService { this.environments = Vue.resource(endpoint); } - all() { + get() { return this.environments.get(); } } diff --git a/app/assets/javascripts/groups_list.js b/app/assets/javascripts/groups_list.js new file mode 100644 index 00000000000..0ef81e49444 --- /dev/null +++ b/app/assets/javascripts/groups_list.js @@ -0,0 +1,47 @@ +/** + * Based on project list search. + * Makes search request for groups when user types a value in the search input. + * Updates the html content of the page with the received one. + */ +export default class GroupsList { + constructor() { + this.groupsListFilterElement = document.querySelector('.js-groups-list-filter'); + this.groupsListHolderElement = document.querySelector('.js-groups-list-holder'); + + this.initSearch(); + } + + initSearch() { + this.debounceFilter = _.debounce(this.filterResults.bind(this), 500); + + this.groupsListFilterElement.removeEventListener('input', this.debounceFilter); + this.groupsListFilterElement.addEventListener('input', this.debounceFilter); + } + + filterResults() { + const form = document.querySelector('form#group-filter-form'); + const groupFilterUrl = `${form.getAttribute('action')}?${$(form).serialize()}`; + + $(this.groupsListHolderElement).fadeTo(250, 0.5); + + return $.ajax({ + url: form.getAttribute('action'), + data: $(form).serialize(), + type: 'GET', + dataType: 'json', + context: this, + complete() { + $(this.groupsListHolderElement).fadeTo(250, 1); + }, + success(data) { + this.groupsListHolderElement.innerHTML = data.html; + + // Change url so if user reload a page - search results are saved + return window.history.replaceState({ + page: groupFilterUrl, + + }, document.title, groupFilterUrl); + }, + }); + } +} diff --git a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 index bf27fbac5d7..357b3487ca9 100644 --- a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 +++ b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 @@ -1,4 +1,6 @@ /* global Vue */ +import stopwatchSvg from 'icons/_icon_stopwatch.svg'; + require('../../../lib/utils/pretty_time'); (() => { @@ -11,7 +13,6 @@ require('../../../lib/utils/pretty_time'); 'showNoTimeTrackingState', 'timeSpentHumanReadable', 'timeEstimateHumanReadable', - 'stopwatchSvg', ], methods: { abbreviateTime(timeStr) { @@ -20,7 +21,7 @@ require('../../../lib/utils/pretty_time'); }, template: ` <div class='sidebar-collapsed-icon'> - <div v-html='stopwatchSvg'></div> + ${stopwatchSvg} <div class='time-tracking-collapsed-summary'> <div class='compare' v-if='showComparisonState'> <span>{{ abbreviateTime(timeSpentHumanReadable) }} / {{ abbreviateTime(timeEstimateHumanReadable) }}</span> diff --git a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 index e38f7852b1c..1fae2d62b14 100644 --- a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 +++ b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 @@ -15,7 +15,6 @@ require('./comparison_pane'); 'time_spent', 'human_time_estimate', 'human_time_spent', - 'stopwatchSvg', 'docsUrl', ], data() { @@ -71,20 +70,19 @@ require('./comparison_pane'); :show-spent-only-state='showSpentOnlyState' :show-estimate-only-state='showEstimateOnlyState' :time-spent-human-readable='timeSpentHumanReadable' - :time-estimate-human-readable='timeEstimateHumanReadable' - :stopwatch-svg='stopwatchSvg'> + :time-estimate-human-readable='timeEstimateHumanReadable'> </time-tracking-collapsed-state> <div class='title hide-collapsed'> Time tracking <div class='help-button pull-right' v-if='!showHelpState' @click='toggleHelpState(true)'> - <i class='fa fa-question-circle'></i> + <i class='fa fa-question-circle' aria-hidden='true'></i> </div> <div class='close-help-button pull-right' v-if='showHelpState' @click='toggleHelpState(false)'> - <i class='fa fa-close'></i> + <i class='fa fa-close' aria-hidden='true'></i> </div> </div> <div class='time-tracking-content hide-collapsed'> diff --git a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 index 1ca01d3bdb9..958a0cc6d50 100644 --- a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 +++ b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 @@ -39,8 +39,9 @@ require('../../subbable_resource'); listenForSlashCommands() { $(document).on('ajax:success', '.gfm-form', (e, data) => { const subscribedCommands = ['spend_time', 'time_estimate']; - const changedCommands = data.commands_changes; - + const changedCommands = data.commands_changes + ? Object.keys(data.commands_changes) + : []; if (changedCommands && _.intersection(subscribedCommands, changedCommands).length) { this.fetchIssuable(); } diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 index 0242350f718..a1423b6fda5 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -247,17 +247,6 @@ }); /** - * Transforms a DOMStringMap into a plain object. - * - * @param {DOMStringMap} DOMStringMapObject - * @returns {Object} - */ - w.gl.utils.DOMStringMapToObject = DOMStringMapObject => Object.keys(DOMStringMapObject).reduce((acc, element) => { - acc[element] = DOMStringMapObject[element]; - return acc; - }, {}); - - /** * Updates the search parameter of a URL given the parameter and values provided. * * If no search params are present we'll add it. diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 579d322e3fb..2e5f8a09fc1 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -65,9 +65,10 @@ require('vendor/latinise'); } }; gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) { - var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine; + var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine; removedLastNewLine = false; removedFirstNewLine = false; + currentLineEmpty = false; // Remove the first newline if (selected.indexOf('\n') === 0) { @@ -82,7 +83,17 @@ require('vendor/latinise'); } selectedSplit = selected.split('\n'); - startChar = !wrap && textArea.selectionStart > 0 ? '\n' : ''; + + if (!wrap) { + lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n'); + + // Check whether the current line is empty or consists only of spaces(=handle as empty) + if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) { + currentLineEmpty = true; + } + } + + startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : ''; if (selectedSplit.length > 1 && (!wrap || (blockTag != null))) { if (blockTag != null) { @@ -142,9 +153,8 @@ require('vendor/latinise'); } }; gl.text.updateText = function(textArea, tag, blockTag, wrap) { - var $textArea, oldVal, selected, text; + var $textArea, selected, text; $textArea = $(textArea); - oldVal = $textArea.val(); textArea = $textArea.get(0); text = $textArea.val(); selected = this.selectedText(text, textArea); diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index 00c6c050612..5f1bd474a0c 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -129,8 +129,9 @@ require('./smart_interval'); }; MergeRequestWidget.prototype.getMergeStatus = function() { - return $.get(this.opts.merge_check_url, function(data) { + return $.get(this.opts.merge_check_url, (data) => { var $html = $(data); + this.updateMergeButton(this.status, this.hasCi, $html); $('.mr-widget-body').replaceWith($html.find('.mr-widget-body')); $('.mr-widget-footer').replaceWith($html.find('.mr-widget-footer')); }); @@ -154,9 +155,9 @@ require('./smart_interval'); return $.getJSON(this.opts.ci_status_url, (function(_this) { return function(data) { var message, status, title; - if (!data.status) { - return; - } + _this.status = data.status; + _this.hasCi = data.has_ci; + _this.updateMergeButton(_this.status, _this.hasCi); if (data.environments && data.environments.length) _this.renderEnvironments(data.environments); if (data.status !== _this.opts.ci_status || data.sha !== _this.opts.ci_sha || @@ -232,36 +233,45 @@ require('./smart_interval'); return; } $('.ci_widget').hide(); - allowed_states = ["failed", "canceled", "running", "pending", "success", "success_with_warnings", "skipped", "not_found"]; - if (indexOf.call(allowed_states, state) !== -1) { - $('.ci_widget.ci-' + state).show(); + $('.ci_widget.ci-' + state).show(); + + this.initMiniPipelineGraph(); + }; + + MergeRequestWidget.prototype.showCICoverage = function(coverage) { + var text = `Coverage ${coverage}%`; + return $('.ci_widget:visible .ci-coverage').text(text); + }; + + MergeRequestWidget.prototype.updateMergeButton = function(state, hasCi, $html) { + const allowed_states = ["failed", "canceled", "running", "pending", "success", "success_with_warnings", "skipped", "not_found"]; + let stateClass = 'btn-danger'; + if (!hasCi) { + stateClass = 'btn-create'; + } else if (indexOf.call(allowed_states, state) !== -1) { switch (state) { case "failed": case "canceled": case "not_found": - this.setMergeButtonClass('btn-danger'); + stateClass = 'btn-danger'; break; case "running": - this.setMergeButtonClass('btn-info'); + stateClass = 'btn-info'; break; case "success": case "success_with_warnings": - this.setMergeButtonClass('btn-create'); + stateClass = 'btn-create'; } } else { $('.ci_widget.ci-error').show(); - this.setMergeButtonClass('btn-danger'); + stateClass = 'btn-danger'; } - }; - MergeRequestWidget.prototype.showCICoverage = function(coverage) { - var text; - text = 'Coverage ' + coverage + '%'; - return $('.ci_widget:visible .ci-coverage').text(text); + this.setMergeButtonClass(stateClass, $html); }; - MergeRequestWidget.prototype.setMergeButtonClass = function(css_class) { - return $('.js-merge-button,.accept-action .dropdown-toggle').removeClass('btn-danger btn-info btn-create').addClass(css_class); + MergeRequestWidget.prototype.setMergeButtonClass = function(css_class, $html = $('.mr-state-widget')) { + return $html.find('.js-merge-button').removeClass('btn-danger btn-info btn-create').addClass(css_class); }; MergeRequestWidget.prototype.updatePipelineUrls = function(id) { diff --git a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 index 5840916846b..21d7c3e168e 100644 --- a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 +++ b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 @@ -15,15 +15,15 @@ }); $(document) - .off('click', '.accept_merge_request') - .on('click', '.accept_merge_request', () => { - $('.js-merge-button').html('<i class="fa fa-spinner fa-spin"></i> Merge in progress'); + .off('click', '.accept-merge-request') + .on('click', '.accept-merge-request', () => { + $('.js-merge-button, .js-merge-when-pipeline-succeeds-button').html('<i class="fa fa-spinner fa-spin"></i> Merge in progress'); }); $(document) - .off('click', '.merge_when_build_succeeds') - .on('click', '.merge_when_build_succeeds', () => { - $('#merge_when_build_succeeds').val('1'); + .off('click', '.merge-when-pipeline-succeeds') + .on('click', '.merge-when-pipeline-succeeds', () => { + $('#merge_when_pipeline_succeeds').val('1'); }); $(document) diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 03504255bda..47fa0f2eb96 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -246,12 +246,21 @@ require('./task_list'); }; Notes.prototype.handleCreateChanges = function(note) { + var votesBlock; if (typeof note === 'undefined') { return; } - if (note.commands_changes && note.commands_changes.indexOf('merge') !== -1) { - $.get(mrRefreshWidgetUrl); + if (note.commands_changes) { + if ('merge' in note.commands_changes) { + $.get(mrRefreshWidgetUrl); + } + + if ('emoji_award' in note.commands_changes) { + votesBlock = $('.js-awards-block').eq(0); + gl.awardsHandler.addAwardToEmojiBar(votesBlock, note.commands_changes.emoji_award); + return gl.awardsHandler.scrollToAwards(); + } } }; @@ -262,26 +271,16 @@ require('./task_list'); */ Notes.prototype.renderNote = function(note) { - var $notesList, votesBlock; + var $notesList; if (!note.valid) { - if (note.award) { - new Flash('You have already awarded this emoji!', 'alert', this.parentTimeline); - } - else { - if (note.errors.commands_only) { - new Flash(note.errors.commands_only, 'notice', this.parentTimeline); - this.refresh(); - } + if (note.errors.commands_only) { + new Flash(note.errors.commands_only, 'notice', this.parentTimeline); + this.refresh(); } return; } - if (note.award) { - votesBlock = $('.js-awards-block').eq(0); - gl.awardsHandler.addAwardToEmojiBar(votesBlock, note.name); - return gl.awardsHandler.scrollToAwards(); - // render note if it not present in loaded list - // or skip if rendered - } else if (this.isNewNote(note)) { + + if (this.isNewNote(note)) { this.note_ids.push(note.id); $notesList = $('ul.main-notes-list'); $notesList.append(note.html).syntaxHighlight(); diff --git a/app/assets/javascripts/profile/gl_crop.js.es6 b/app/assets/javascripts/profile/gl_crop.js.es6 index 42e9847af91..192b1192d07 100644 --- a/app/assets/javascripts/profile/gl_crop.js.es6 +++ b/app/assets/javascripts/profile/gl_crop.js.es6 @@ -13,7 +13,7 @@ this.onPickImageClick = this.onPickImageClick.bind(this); this.fileInput = $(input); this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg; - this.fileInput.attr('name', `${this.fileInput.attr('name')}-trigger`).attr('id', `this.fileInput.attr('id')-trigger`); + this.fileInput.attr('name', `${this.fileInput.attr('name')}-trigger`).attr('id', `${this.fileInput.attr('id')}-trigger`); this.exportWidth = exportWidth; this.exportHeight = exportHeight; this.cropBoxWidth = cropBoxWidth; diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js index 542cd586df0..73db8c10b99 100644 --- a/app/assets/javascripts/shortcuts_navigation.js +++ b/app/assets/javascripts/shortcuts_navigation.js @@ -16,9 +16,6 @@ require('./shortcuts'); Mousetrap.bind('g p', function() { return ShortcutsNavigation.findAndFollowLink('.shortcuts-project'); }); - Mousetrap.bind('g e', function() { - return ShortcutsNavigation.findAndFollowLink('.shortcuts-project-activity'); - }); Mousetrap.bind('g f', function() { return ShortcutsNavigation.findAndFollowLink('.shortcuts-tree'); }); @@ -31,9 +28,6 @@ require('./shortcuts'); Mousetrap.bind('g n', function() { return ShortcutsNavigation.findAndFollowLink('.shortcuts-network'); }); - Mousetrap.bind('g g', function() { - return ShortcutsNavigation.findAndFollowLink('.shortcuts-graphs'); - }); Mousetrap.bind('g i', function() { return ShortcutsNavigation.findAndFollowLink('.shortcuts-issues'); }); diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js index 74b869502a4..99419e85b20 100644 --- a/app/assets/javascripts/user_callout.js +++ b/app/assets/javascripts/user_callout.js @@ -43,6 +43,8 @@ class UserCallout { this.userCalloutBody.append($template); $template.find(closeButton).on('click', e => this.dismissCallout(e)); $template.find(userCalloutBtn).on('click', e => this.dismissCallout(e)); + } else { + this.userCalloutBody.remove(); } } @@ -50,7 +52,7 @@ class UserCallout { Cookies.set(USER_CALLOUT_COOKIE, 'true'); const $currentTarget = $(e.currentTarget); if ($currentTarget.hasClass('close-user-callout')) { - this.userCalloutBody.empty(); + this.userCalloutBody.remove(); } } } diff --git a/app/assets/javascripts/vue_pipelines_index/index.js.es6 b/app/assets/javascripts/vue_pipelines_index/index.js.es6 index e7432afb56e..a90bd1518e9 100644 --- a/app/assets/javascripts/vue_pipelines_index/index.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/index.js.es6 @@ -11,15 +11,10 @@ $(() => new Vue({ data() { const project = document.querySelector('.pipelines'); - const svgs = document.querySelector('.pipeline-svgs').dataset; - - // Transform svgs DOMStringMap to a plain Object. - const svgsObject = gl.utils.DOMStringMapToObject(svgs); return { scope: project.dataset.url, store: new gl.PipelineStore(), - svgs: svgsObject, }; }, components: { @@ -27,10 +22,8 @@ $(() => new Vue({ }, template: ` <vue-pipelines - :scope='scope' - :store='store' - :svgs='svgs' - > + :scope="scope" + :store="store"> </vue-pipelines> `, })); diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 index e20085d1fd2..891f1f17fb3 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 @@ -1,9 +1,10 @@ /* global Vue, Flash, gl */ -/* eslint-disable no-param-reassign, no-alert */ +/* eslint-disable no-param-reassign, no-alert */ +const playIconSvg = require('icons/_icon_play.svg'); ((gl) => { gl.VuePipelineActions = Vue.extend({ - props: ['pipeline', 'svgs'], + props: ['pipeline'], computed: { actions() { return this.pipeline.details.manual_actions.length > 0; @@ -31,8 +32,13 @@ } }, }, + + data() { + return { playIconSvg }; + }, + template: ` - <td class="pipeline-actions hidden-xs"> + <td class="pipeline-actions"> <div class="pull-right"> <div class="btn-group"> <div class="btn-group" v-if="actions"> @@ -42,7 +48,7 @@ title="Manual job" data-placement="top" aria-label="Manual job"> - <span v-html="svgs.iconPlay" aria-hidden="true"></span> + <span v-html="playIconSvg" aria-hidden="true"></span> <i class="fa fa-caret-down" aria-hidden="true"></i> </button> <ul class="dropdown-menu dropdown-menu-align-right"> @@ -50,8 +56,8 @@ <a rel="nofollow" data-method="post" - :href="action.path"> - <span v-html="svgs.iconPlay" aria-hidden="true"></span> + :href="action.path" > + <span v-html="playIconSvg" aria-hidden="true"></span> <span>{{action.name}}</span> </a> </li> diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 index 9d66d28cc62..601ef41e917 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 @@ -27,7 +27,7 @@ const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_s pageRequest: false, }; }, - props: ['scope', 'store', 'svgs'], + props: ['scope', 'store'], created() { const pagenum = gl.utils.getParameterByName('page'); const scope = gl.utils.getParameterByName('scope'); @@ -45,18 +45,15 @@ const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_s methods: { /** - * Changes the URL according to the pagination component. + * Will change the page number and update the URL. * - * If no scope is provided, 'all' is assumed. - * - * Pagination component sends "null" when no scope is provided. - * - * @param {Number} pagenum - * @param {String} apiScope = 'all' + * @param {Number} pageNumber desired page to go to. */ - change(pagenum, apiScope) { - if (!apiScope) apiScope = 'all'; - gl.utils.visitUrl(`?scope=${apiScope}&page=${pagenum}`); + change(pageNumber) { + const param = gl.utils.setParamInURL('page', pageNumber); + + gl.utils.visitUrl(param); + return param; }, }, template: ` @@ -73,10 +70,7 @@ const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_s </div> <div class="table-holder" v-if='!pageRequest && pipelines.length'> - <pipelines-table-component - :pipelines='pipelines' - :svgs='svgs'> - </pipelines-table-component> + <pipelines-table-component :pipelines='pipelines'/> </div> <gl-pagination diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 index 67fdd729e41..f67ebd6a265 100644 --- a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 @@ -1,27 +1,42 @@ /* global Vue, Flash, gl */ /* eslint-disable no-param-reassign */ +import canceledSvg from 'icons/_icon_status_canceled_borderless.svg'; +import createdSvg from 'icons/_icon_status_created_borderless.svg'; +import failedSvg from 'icons/_icon_status_failed_borderless.svg'; +import manualSvg from 'icons/_icon_status_manual_borderless.svg'; +import pendingSvg from 'icons/_icon_status_pending_borderless.svg'; +import runningSvg from 'icons/_icon_status_running_borderless.svg'; +import skippedSvg from 'icons/_icon_status_skipped_borderless.svg'; +import successSvg from 'icons/_icon_status_success_borderless.svg'; +import warningSvg from 'icons/_icon_status_warning_borderless.svg'; ((gl) => { gl.VueStage = Vue.extend({ data() { + const svgsDictionary = { + icon_status_canceled: canceledSvg, + icon_status_created: createdSvg, + icon_status_failed: failedSvg, + icon_status_manual: manualSvg, + icon_status_pending: pendingSvg, + icon_status_running: runningSvg, + icon_status_skipped: skippedSvg, + icon_status_success: successSvg, + icon_status_warning: warningSvg, + }; + return { builds: '', spinner: '<span class="fa fa-spinner fa-spin"></span>', + svg: svgsDictionary[this.stage.status.icon], }; }, + props: { stage: { type: Object, required: true, }, - svgs: { - type: Object, - required: true, - }, - match: { - type: Function, - required: true, - }, }, updated() { @@ -73,11 +88,6 @@ tooltip() { return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`; }, - svg() { - const { icon } = this.stage.status; - const stageIcon = icon.replace(/icon/i, 'stage_icon'); - return this.svgs[this.match(stageIcon)]; - }, triggerButtonClass() { return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`; }, @@ -91,8 +101,7 @@ data-placement="top" data-toggle="dropdown" type="button" - :aria-label="stage.title" - > + :aria-label="stage.title"> <span v-html="svg" aria-hidden="true"></span> <i class="fa fa-caret-down" aria-hidden="true"></i> </button> @@ -101,8 +110,7 @@ <div :class="dropdownClass" class="js-builds-dropdown-list scrollable-menu" - v-html="buildsOrSpinner" - > + v-html="buildsOrSpinner"> </div> </ul> </div> diff --git a/app/assets/javascripts/vue_pipelines_index/status.js.es6 b/app/assets/javascripts/vue_pipelines_index/status.js.es6 index 05175082704..8d9f83ac113 100644 --- a/app/assets/javascripts/vue_pipelines_index/status.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/status.js.es6 @@ -1,32 +1,62 @@ /* global Vue, gl */ /* eslint-disable no-param-reassign */ +import canceledSvg from 'icons/_icon_status_canceled.svg'; +import createdSvg from 'icons/_icon_status_created.svg'; +import failedSvg from 'icons/_icon_status_failed.svg'; +import manualSvg from 'icons/_icon_status_manual.svg'; +import pendingSvg from 'icons/_icon_status_pending.svg'; +import runningSvg from 'icons/_icon_status_running.svg'; +import skippedSvg from 'icons/_icon_status_skipped.svg'; +import successSvg from 'icons/_icon_status_success.svg'; +import warningSvg from 'icons/_icon_status_warning.svg'; + ((gl) => { gl.VueStatusScope = Vue.extend({ props: [ - 'pipeline', 'svgs', 'match', + 'pipeline', ], + + data() { + const svgsDictionary = { + icon_status_canceled: canceledSvg, + icon_status_created: createdSvg, + icon_status_failed: failedSvg, + icon_status_manual: manualSvg, + icon_status_pending: pendingSvg, + icon_status_running: runningSvg, + icon_status_skipped: skippedSvg, + icon_status_success: successSvg, + icon_status_warning: warningSvg, + }; + + return { + svg: svgsDictionary[this.pipeline.details.status.icon], + }; + }, + computed: { cssClasses() { const cssObject = { 'ci-status': true }; cssObject[`ci-${this.pipeline.details.status.group}`] = true; return cssObject; }, - svg() { - return this.svgs[this.match(this.pipeline.details.status.icon)]; - }, + detailsPath() { const { status } = this.pipeline.details; return status.has_details ? status.details_path : false; }, + + content() { + return `${this.svg} ${this.pipeline.details.status.text}`; + }, }, template: ` <td class="commit-link"> <a - :class='cssClasses' - :href='detailsPath' - v-html='svg + pipeline.details.status.text' - > + :class="cssClasses" + :href="detailsPath" + v-html="content"> </a> </td> `, diff --git a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 index 6048fa691dc..a383570857d 100644 --- a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 @@ -4,14 +4,17 @@ window.Vue = require('vue'); require('../lib/utils/datetime_utility'); +const iconTimerSvg = require('../../../views/shared/icons/_icon_timer.svg'); + ((gl) => { gl.VueTimeAgo = Vue.extend({ data() { return { currentTime: new Date(), + iconTimerSvg, }; }, - props: ['pipeline', 'svgs'], + props: ['pipeline'], computed: { timeAgo() { return gl.utils.getTimeago(); @@ -56,7 +59,7 @@ require('../lib/utils/datetime_utility'); template: ` <td class="pipelines-time-ago"> <p class="duration" v-if='duration'> - <span v-html='svgs.iconTimer'></span> + <span v-html="iconTimerSvg"></span> {{duration}} </p> <p class="finished-at" v-if='timeStopped'> diff --git a/app/assets/javascripts/vue_shared/components/commit.js.es6 b/app/assets/javascripts/vue_shared/components/commit.js.es6 index ff88e236829..4381487b79e 100644 --- a/app/assets/javascripts/vue_shared/components/commit.js.es6 +++ b/app/assets/javascripts/vue_shared/components/commit.js.es6 @@ -1,5 +1,6 @@ /* global Vue */ window.Vue = require('vue'); +const commitIconSvg = require('icons/_icon_commit.svg'); (() => { window.gl = window.gl || {}; @@ -69,11 +70,6 @@ window.Vue = require('vue'); required: false, default: () => ({}), }, - - commitIconSvg: { - type: String, - required: false, - }, }, computed: { @@ -116,6 +112,10 @@ window.Vue = require('vue'); }, }, + data() { + return { commitIconSvg }; + }, + template: ` <div class="branch-commit"> diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 index 4bdaef31ee9..0d8f85db965 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 +++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 @@ -21,14 +21,6 @@ require('./pipelines_table_row'); default: () => ([]), }, - /** - * TODO: Remove this when we have webpack. - */ - svgs: { - type: Object, - required: true, - default: () => ({}), - }, }, components: { @@ -44,15 +36,14 @@ require('./pipelines_table_row'); <th class="js-pipeline-commit pipeline-commit">Commit</th> <th class="js-pipeline-stages pipeline-stages">Stages</th> <th class="js-pipeline-date pipeline-date"></th> - <th class="js-pipeline-actions pipeline-actions hidden-xs"></th> + <th class="js-pipeline-actions pipeline-actions"></th> </tr> </thead> <tbody> <template v-for="model in pipelines" v-bind:model="model"> <tr is="pipelines-table-row-component" - :pipeline="model" - :svgs="svgs"></tr> + :pipeline="model"></tr> </template> </tbody> </table> diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 index 61c1b72d9d2..e5e88186a85 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 +++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 @@ -25,14 +25,6 @@ require('./commit'); default: () => ({}), }, - /** - * TODO: Remove this when we have webpack; - */ - svgs: { - type: Object, - required: true, - default: () => ({}), - }, }, components: { @@ -174,30 +166,9 @@ require('./commit'); }, }, - methods: { - /** - * FIXME: This should not be in this component but in the components that - * need this function. - * - * Used to render SVGs in the following components: - * - status-scope - * - dropdown-stage - * - * @param {String} string - * @return {String} - */ - match(string) { - return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase()); - }, - }, - template: ` <tr class="commit"> - <status-scope - :pipeline="pipeline" - :svgs="svgs" - :match="match"> - </status-scope> + <status-scope :pipeline="pipeline"/> <pipeline-url :pipeline="pipeline"></pipeline-url> @@ -208,26 +179,20 @@ require('./commit'); :commit-url="commitUrl" :short-sha="commitShortSha" :title="commitTitle" - :author="commitAuthor" - :commit-icon-svg="svgs.commitIconSvg"> - </commit-component> + :author="commitAuthor"/> </td> <td class="stage-cell"> <div class="stage-container dropdown js-mini-pipeline-graph" v-if="pipeline.details.stages.length > 0" v-for="stage in pipeline.details.stages"> - <dropdown-stage - :stage="stage" - :svgs="svgs" - :match="match"> - </dropdown-stage> + <dropdown-stage :stage="stage"/> </div> </td> - <time-ago :pipeline="pipeline" :svgs="svgs"></time-ago> + <time-ago :pipeline="pipeline"/> - <pipeline-actions :pipeline="pipeline" :svgs="svgs"></pipeline-actions> + <pipeline-actions :pipeline="pipeline" /> </tr> `, }); diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 index d8042a9b7fc..8943b850a72 100644 --- a/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 +++ b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 @@ -19,12 +19,11 @@ window.Vue = require('vue'); /** This function will take the information given by the pagination component - And make a new Turbolinks call Here is an example `change` method: - change(pagenum, apiScope) { - gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`); + change(pagenum) { + gl.utils.visitUrl(`?page=${pagenum}`); }, */ @@ -57,8 +56,6 @@ window.Vue = require('vue'); }, methods: { changePage(e) { - const apiScope = gl.utils.getParameterByName('scope'); - const text = e.target.innerText; const { totalPages, nextPage, previousPage } = this.pageInfo; @@ -66,19 +63,19 @@ window.Vue = require('vue'); case SPREAD: break; case LAST: - this.change(totalPages, apiScope); + this.change(totalPages); break; case NEXT: - this.change(nextPage, apiScope); + this.change(nextPage); break; case PREV: - this.change(previousPage, apiScope); + this.change(previousPage); break; case FIRST: - this.change(1, apiScope); + this.change(1); break; default: - this.change(+text, apiScope); + this.change(+text); break; } }, diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index fb8ea18d122..9a0f7a14e57 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -1,6 +1,7 @@ .calender-block { padding-left: 0; padding-right: 0; + border-top: 0; direction: rtl; @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) { diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index ff31e7f7b3d..6e8a5cc688b 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -107,11 +107,12 @@ &.fa-spinner { font-size: 16px; - margin-top: -8px; + margin-top: -3px; } } - .fa-chevron-down { + .fa-chevron-down, + .fa-spinner { position: absolute; top: 11px; right: 8px; @@ -192,6 +193,10 @@ &.is-focused { background-color: $dropdown-link-hover-bg; text-decoration: none; + + .badge { + background-color: darken($row-hover, 5%); + } } &.dropdown-menu-empty-link { @@ -228,6 +233,12 @@ padding: 5px 8px; color: $gl-text-color-secondary; } + + .badge { + position: absolute; + right: 8px; + top: 5px; + } } .dropdown-menu-drop-up { diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 30f242a35db..ffece53a093 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -271,6 +271,7 @@ span.idiff { font-size: 13px; line-height: 28px; display: inline-block; + float: none; } } } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 3945a789c82..f4316ec7022 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -148,34 +148,20 @@ header { } .header-logo { - position: absolute; - left: 50%; - top: 7px; + display: inline-block; + margin: 0 7px 0 2px; + position: relative; + top: 10px; transition-duration: .3s; - z-index: 999; - - #logo { - position: relative; - left: -50%; - } svg, img { - height: 36px; + height: 28px; } &:hover { cursor: pointer; } - - @media (max-width: $screen-xs-max) { - right: 20px; - left: auto; - - #logo { - left: auto; - } - } } .title { @@ -183,7 +169,6 @@ header { padding-right: 20px; margin: 0; font-size: 18px; - max-width: 385px; display: inline-block; line-height: $header-height; font-weight: normal; @@ -193,14 +178,18 @@ header { vertical-align: top; white-space: nowrap; - @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { - max-width: 300px; - } - @media (max-width: $screen-xs-max) { max-width: 190px; } + @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) { + max-width: 428px; + } + + @media (min-width: $screen-lg-min) { + max-width: 685px; + } + a { color: $gl-text-color; diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 8978e284f55..40e93032f59 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -73,10 +73,6 @@ right: $gutter_collapsed_width; } } - - &.with-overlay { - padding-right: $gutter_collapsed_width; - } } .right-sidebar { diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index 9c76e58dfc8..09951fe3d3e 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -21,6 +21,7 @@ $dark-highlight-color: $black; $dark-pre-hll-bg: #373b41; $dark-hll-bg: #373b41; $dark-over-bg: #9f9ab5; +$dark-expanded-bg: #3e3e3e; $dark-c: #969896; $dark-err: #c66; $dark-k: #b294bb; @@ -155,6 +156,22 @@ $dark-il: #de935f; .line_content.match { @include dark-diff-match-line; } + + &:not(.diff-expanded) + .diff-expanded, + &.diff-expanded + .line_holder:not(.diff-expanded) { + > .diff-line-num, + > .line_content { + border-top: 1px solid $black; + } + } + + &.diff-expanded { + > .diff-line-num, + > .line_content { + background: $dark-expanded-bg; + border-color: $dark-expanded-bg; + } + } } // highlight line via anchor diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss index 6488a099c74..b6a6d298adf 100644 --- a/app/assets/stylesheets/highlight/monokai.scss +++ b/app/assets/stylesheets/highlight/monokai.scss @@ -14,6 +14,7 @@ $monokai-line-empty-border: darken($monokai-line-empty-bg, 15%); $monokai-diff-border: #808080; $monokai-highlight-bg: #ffe792; $monokai-over-bg: #9f9ab5; +$monokai-expanded-bg: #3e3e3e; $monokai-new-bg: rgba(166, 226, 46, 0.1); $monokai-new-idiff: rgba(166, 226, 46, 0.15); @@ -155,6 +156,22 @@ $monokai-gi: #a6e22e; .line_content.match { @include dark-diff-match-line; } + + &:not(.diff-expanded) + .diff-expanded, + &.diff-expanded + .line_holder:not(.diff-expanded) { + > .diff-line-num, + > .line_content { + border-top: 1px solid $black; + } + } + + &.diff-expanded { + > .diff-line-num, + > .line_content { + background: $monokai-expanded-bg; + border-color: $monokai-expanded-bg; + } + } } // highlight line via anchor diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss index 00079cc2b5b..4f7a50dcb4f 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -18,6 +18,7 @@ $solarized-dark-line-color-old: #7a6c71; $solarized-dark-highlight: #094554; $solarized-dark-hll-bg: #174652; $solarized-dark-over-bg: #9f9ab5; +$solarized-dark-expanded-bg: #010d10; $solarized-dark-c: #586e75; $solarized-dark-err: #93a1a1; $solarized-dark-g: #93a1a1; @@ -159,6 +160,22 @@ $solarized-dark-il: #2aa198; .line_content.match { @include dark-diff-match-line; } + + &:not(.diff-expanded) + .diff-expanded, + &.diff-expanded + .line_holder:not(.diff-expanded) { + > .diff-line-num, + > .line_content { + border-top: 1px solid $black; + } + } + + &.diff-expanded { + > .diff-line-num, + > .line_content { + background: $solarized-dark-expanded-bg; + border-color: $solarized-dark-expanded-bg; + } + } } // highlight line via anchor diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss index 2e3b68f1a58..6463fe96c1b 100644 --- a/app/assets/stylesheets/highlight/solarized_light.scss +++ b/app/assets/stylesheets/highlight/solarized_light.scss @@ -19,6 +19,8 @@ $solarized-light-line-color-old: #ad9186; $solarized-light-highlight: #eee8d5; $solarized-light-hll-bg: #ddd8c5; $solarized-light-over-bg: #ded7fc; +$solarized-light-expanded-border: #d2cdbd; +$solarized-light-expanded-bg: #ece6d4; $solarized-light-c: #93a1a1; $solarized-light-err: #586e75; $solarized-light-g: #586e75; @@ -166,6 +168,22 @@ $solarized-light-il: #2aa198; .line_content.match { @include matchLine; } + + &:not(.diff-expanded) + .diff-expanded, + &.diff-expanded + .line_holder:not(.diff-expanded) { + > .diff-line-num, + > .line_content { + border-top: 1px solid $solarized-light-expanded-border; + } + } + + &.diff-expanded { + > .diff-line-num, + > .line_content { + background: $solarized-light-expanded-bg; + border-color: $solarized-light-expanded-bg; + } + } } // highlight line via anchor diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index 0eca30e570f..ab2018bfbca 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -8,6 +8,8 @@ $white-highlight: #fafe3d; $white-pre-hll-bg: #f8eec7; $white-hll-bg: #f8f8f8; $white-over-bg: #ded7fc; +$white-expanded-border: #e0e0e0; +$white-expanded-bg: #f7f7f7; $white-c: #998; $white-err: #a61717; $white-err-bg: #e3d2d2; @@ -140,6 +142,22 @@ $white-gc-bg: #eaf2f5; } } + &:not(.diff-expanded) + .diff-expanded, + &.diff-expanded + .line_holder:not(.diff-expanded) { + > .diff-line-num, + > .line_content { + border-top: 1px solid $white-expanded-border; + } + } + + &.diff-expanded { + > .diff-line-num, + > .line_content { + background: $white-expanded-bg; + border-color: $white-expanded-bg; + } + } + .line_content { &.old { background-color: $line-removed; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 339cdcde480..5d0c247dea8 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -133,8 +133,13 @@ width: 35px; font-weight: normal; - &:hover { - text-decoration: underline; + &[disabled] { + cursor: default; + + &:hover, + &:active { + text-decoration: none; + } } } } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index f789ae1ccd3..77e09e66340 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -15,112 +15,97 @@ padding-top: 20px; } -@media (max-width: $screen-xs-max) { - .environments-container { +.environments-container { + .table-holder { width: 100%; overflow: auto; } -} - -.environments { - table-layout: fixed; - - .environments-commit, - .environments-actions, - .environments-deploy, - .environments-build, - .environments-date { - position: static; - float: none; - display: table-cell; - } - - .environments-commit, - .environments-actions { - width: 20%; - } - - .environments-date { - width: 10%; - } - .environments-name, - .environments-deploy, - .environments-build { - width: 15%; - } - - .environment-name, - .environments-build-cell, - .deployment-column { - word-break: break-all; - } - - .deployment-column { - .avatar { - float: none; + .table.ci-table { + .environments-actions { + min-width: 200px; } - } - .btn-group { + .environments-commit, + .environments-actions { + width: 20%; + } - > a { - color: $gl-text-color-secondary; + .environments-date { + width: 10%; } - svg path { - fill: $gl-text-color-secondary; + .environments-name, + .environments-deploy, + .environments-build { + width: 15%; } - .dropdown { - outline: none; + .deployment-column { + > span { + word-break: break-all; + } + + .avatar { + float: none; + } } - } + .btn-group { - .commit-title { - margin: 0; - } + > a { + color: $gl-text-color-secondary; + } - .avatar-image-container { - text-decoration: none; - } + svg path { + fill: $gl-text-color-secondary; + } - .icon-play { - height: 13px; - width: 12px; - } + .dropdown { + outline: none; + } + } - .external-url, - .dropdown-new { - color: $gl-text-color-secondary; - } + .commit-title { + margin: 0; + } - .dropdown-menu { + .avatar-image-container { + text-decoration: none; + } - .fa { - margin-right: 6px; - color: $gl-text-color-secondary; + .icon-play { + height: 13px; + width: 12px; } - } - .build-link, - .branch-name { - color: $gl-text-color; - } + .external-url, + .dropdown-new { + color: $gl-text-color-secondary; + } - .stop-env-link, - .external-url { - color: $gl-text-color-secondary; + .dropdown-menu { + .fa { + margin-right: 6px; + color: $gl-text-color-secondary; + } + } - .stop-env-icon { - font-size: 14px; + .build-link, + .branch-name { + color: $gl-text-color; } - } - .deployment { - .build-column { + .stop-env-link, + .external-url { + color: $gl-text-color-secondary; + + .stop-env-icon { + font-size: 14px; + } + } + .deployment .build-column { .build-link { color: $gl-text-color; } @@ -129,34 +114,32 @@ float: none; } } - } - - .folder-icon { - margin-right: 3px; - color: $gl-text-color-secondary; - display: inline-block; - .fa:nth-child(1) { + .folder-icon { margin-right: 3px; + color: $gl-text-color-secondary; + display: inline-block; + + .fa:nth-child(1) { + margin-right: 3px; + } } - } - .folder-name { - cursor: pointer; - color: $gl-text-color-secondary; - display: inline-block; - } -} + .folder-name { + cursor: pointer; + color: $gl-text-color-secondary; + display: inline-block; + } -.table.ci-table.environments { - .icon-container { - width: 20px; - text-align: center; - } + .icon-container { + width: 20px; + text-align: center; + } - .branch-commit { - .commit-id { - margin-right: 0; + .branch-commit { + .commit-id { + margin-right: 0; + } } } } diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 5776d86983a..08398bb43a2 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -155,7 +155,7 @@ @media (max-width: $screen-xs-max) { .event-item { - padding-left: $gl-padding; + padding-left: 0; .event-title { white-space: normal; @@ -169,8 +169,7 @@ .event-body { margin: 0; - border-left: 2px solid $events-body-border; - padding-left: 10px; + padding-left: 0; } .event-item-timestamp { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index ad585ff2892..a849d93331b 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -28,7 +28,7 @@ background-color: $gl-success; } - .accept_merge_request { + .accept-merge-request { &.ci-pending, &.ci-running { @include btn-blue; @@ -41,6 +41,12 @@ @include btn-red; } } + + .dropdown-toggle { + .fa { + color: inherit; + } + } } .accept-control { diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index f4707f71208..69eea1b2217 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -105,6 +105,7 @@ @media (max-width: $screen-md-max) { .content-list { &.pipelines, + &.environments-container, &.builds-content-list { width: 100%; overflow: auto; diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index aad1a8986b0..1a983d8c9ef 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -279,7 +279,7 @@ table.u2f-registrations { } .user-callout { - margin: 24px auto 0; + margin: 0 auto; .bordered-box { border: 1px solid $border-color; @@ -287,6 +287,7 @@ table.u2f-registrations { } .landing { + margin-top: $gl-padding; margin-bottom: $gl-padding; .close { diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index e4487dbcb87..8d1063fc26f 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -178,3 +178,29 @@ margin-left: $btn-side-margin; } } + +.repo-charts { + .sub-header { + margin: 20px 0; + } + + .sub-header-block.border-top { + margin-top: 20px; + padding: 0; + border-top: 1px solid $white-dark; + border-bottom: none; + } + + .commit-stats li { + font-size: 16px; + } + + .tree-ref-header { + margin-bottom: 20px; + + h4 { + margin: 0; + line-height: 36px; + } + } +} diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index e42e48f87d2..cc7b7f247e8 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -72,14 +72,6 @@ class ApplicationController < ActionController::Base end end - def authenticate_user!(*args) - if redirect_to_home_page_url? - return redirect_to current_application_settings.home_page_url - end - - super(*args) - end - def log_exception(exception) application_trace = ActionDispatch::ExceptionWrapper.new(env, exception).application_trace application_trace.map!{ |t| " #{t}\n" } @@ -130,10 +122,6 @@ class ApplicationController < ActionController::Base headers['X-XSS-Protection'] = '1; mode=block' headers['X-UA-Compatible'] = 'IE=edge' headers['X-Content-Type-Options'] = 'nosniff' - # Enabling HSTS for non-standard ports would send clients to the wrong port - if Gitlab.config.gitlab.https && Gitlab.config.gitlab.port == 443 - headers['Strict-Transport-Security'] = 'max-age=31536000' - end end def validate_user_service_ticket! @@ -287,19 +275,6 @@ class ApplicationController < ActionController::Base session[:skip_tfa] && session[:skip_tfa] > Time.current end - def redirect_to_home_page_url? - # If user is not signed-in and tries to access root_path - redirect him to landing page - # Don't redirect to the default URL to prevent endless redirections - return false unless current_application_settings.home_page_url.present? - - home_page_url = current_application_settings.home_page_url.chomp('/') - root_urls = [Gitlab.config.gitlab['url'].chomp('/'), root_url.chomp('/')] - - return false if root_urls.include?(home_page_url) - - current_user.nil? && root_path == request.path - end - # U2F (universal 2nd factor) devices need a unique identifier for the application # to perform authentication. # https://developers.yubico.com/U2F/App_ID.html diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb deleted file mode 100644 index ff297d6ff13..00000000000 --- a/app/controllers/ci/projects_controller.rb +++ /dev/null @@ -1,47 +0,0 @@ -module Ci - class ProjectsController < ::ApplicationController - before_action :project - before_action :no_cache, only: [:badge] - before_action :authorize_read_project!, except: [:badge, :index] - skip_before_action :authenticate_user!, only: [:badge] - protect_from_forgery - - def index - redirect_to root_path - end - - def show - # Temporary compatibility with CI badges pointing to CI project page - redirect_to namespace_project_path(project.namespace, project) - end - - # Project status badge - # Image with build status for sha or ref - # - # This action in DEPRECATED, this is here only for backwards compatibility - # with projects migrated from GitLab CI. - # - def badge - return render_404 unless @project - - image = Ci::ImageForBuildService.new.execute(@project, params) - send_file image.path, filename: image.name, disposition: 'inline', type: "image/svg+xml" - end - - protected - - def project - @project ||= Project.find_by(ci_id: params[:id].to_i) - end - - def no_cache - response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate" - response.headers["Pragma"] = "no-cache" - response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT" - end - - def authorize_read_project! - return access_denied! unless can?(current_user, :read_project, project) - end - end -end diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 2fe03020d2d..9ac8197e45a 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -4,10 +4,9 @@ module CreatesCommit def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil) set_commit_variables - start_branch = @mr_target_branch unless initial_commit? commit_params = @commit_params.merge( start_project: @mr_target_project, - start_branch: start_branch, + start_branch: @mr_target_branch, target_branch: @mr_source_branch ) @@ -17,12 +16,16 @@ module CreatesCommit if result[:status] == :success update_flash_notice(success_notice) + success_path = final_success_path(success_path) + respond_to do |format| - format.html { redirect_to final_success_path(success_path) } - format.json { render json: { message: "success", filePath: final_success_path(success_path) } } + format.html { redirect_to success_path } + format.json { render json: { message: "success", filePath: success_path } } end else flash[:alert] = result[:message] + failure_path = failure_path.call if failure_path.respond_to?(:call) + respond_to do |format| format.html do if failure_view @@ -58,9 +61,13 @@ module CreatesCommit end def final_success_path(success_path) - return success_path unless create_merge_request? + if create_merge_request? + merge_request_exists? ? existing_merge_request_path : new_merge_request_path + else + success_path = success_path.call if success_path.respond_to?(:call) - merge_request_exists? ? existing_merge_request_path : new_merge_request_path + success_path + end end def new_merge_request_path @@ -92,47 +99,26 @@ module CreatesCommit end def create_merge_request? - # XXX: Even if the field is set, if we're checking the same branch + # Even if the field is set, if we're checking the same branch # as the target branch in the same project, # we don't want to create a merge request. params[:create_merge_request].present? && - (different_project? || @ref != @target_branch) + (different_project? || @mr_target_branch != @mr_source_branch) end - # TODO: We should really clean this up def set_commit_variables - @mr_source_project = - if can?(current_user, :push_code, @project) - # Edit file in this project - @project - else - # Merge request from fork to this project - current_user.fork_of(@project) - end + if can?(current_user, :push_code, @project) + @mr_source_project = @project + @target_branch ||= @ref + else + @mr_source_project = current_user.fork_of(@project) + @target_branch ||= @mr_source_project.repository.next_branch('patch') + end # Merge request to this project @mr_target_project = @project - @mr_target_branch = @ref || @target_branch - - @mr_source_branch = guess_mr_source_branch - end - - def initial_commit? - @mr_target_branch.nil? || - !@mr_target_project.repository.branch_exists?(@mr_target_branch) - end + @mr_target_branch ||= @ref || @target_branch - def guess_mr_source_branch - # XXX: Happens when viewing a commit without a branch. In this case, - # @target_branch would be the default branch for @mr_source_project, - # however we want a generated new branch here. Thus we can't use - # @target_branch, but should pass nil to indicate that we want a new - # branch instead of @target_branch. - return if - create_merge_request? && - # XXX: Don't understand why rubocop prefers this indention - @mr_source_project.repository.branch_exists?(@target_branch) - - @target_branch + @mr_source_branch = @target_branch end end diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb index 0b7cf8167f0..d03265e9f20 100644 --- a/app/controllers/dashboard/groups_controller.rb +++ b/app/controllers/dashboard/groups_controller.rb @@ -1,5 +1,17 @@ class Dashboard::GroupsController < Dashboard::ApplicationController def index - @group_members = current_user.group_members.includes(source: :route).page(params[:page]) + @group_members = current_user.group_members.includes(source: :route).joins(:group) + @group_members = @group_members.merge(Group.search(params[:filter_groups])) if params[:filter_groups].present? + @group_members = @group_members.merge(Group.sort(@sort = params[:sort])) + @group_members = @group_members.page(params[:page]) + + respond_to do |format| + format.html + format.json do + render json: { + html: view_to_html_string("dashboard/groups/_groups", locals: { group_members: @group_members }) + } + end + end end end diff --git a/app/controllers/explore/groups_controller.rb b/app/controllers/explore/groups_controller.rb index a962f9a0937..68228c095da 100644 --- a/app/controllers/explore/groups_controller.rb +++ b/app/controllers/explore/groups_controller.rb @@ -1,8 +1,17 @@ class Explore::GroupsController < Explore::ApplicationController def index @groups = GroupsFinder.new.execute(current_user) - @groups = @groups.search(params[:search]) if params[:search].present? + @groups = @groups.search(params[:filter_groups]) if params[:filter_groups].present? @groups = @groups.sort(@sort = params[:sort]) @groups = @groups.page(params[:page]) + + respond_to do |format| + format.html + format.json do + render json: { + html: view_to_html_string("explore/groups/_groups", locals: { groups: @groups }) + } + end + end end end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 39ba815cfca..21ed0660762 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -5,7 +5,7 @@ class Projects::BlobController < Projects::ApplicationController include ActionView::Helpers::SanitizeHelper # Raised when given an invalid file path - class InvalidPathError < StandardError; end + InvalidPathError = Class.new(StandardError) before_action :require_non_empty_project, except: [:new, :create] before_action :authorize_download_code! @@ -24,7 +24,7 @@ class Projects::BlobController < Projects::ApplicationController def create create_commit(Files::CreateService, success_notice: "The file has been successfully created.", - success_path: namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)), + success_path: -> { namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) }, failure_view: :new, failure_path: namespace_project_new_blob_path(@project.namespace, @project, @ref)) end @@ -40,7 +40,7 @@ class Projects::BlobController < Projects::ApplicationController def update @path = params[:file_path] if params[:file_path].present? - create_commit(Files::UpdateService, success_path: after_edit_path, + create_commit(Files::UpdateService, success_path: -> { after_edit_path }, failure_view: :edit, failure_path: namespace_project_blob_path(@project.namespace, @project, @id)) @@ -62,7 +62,7 @@ class Projects::BlobController < Projects::ApplicationController def destroy create_commit(Files::DestroyService, success_notice: "The file has been successfully deleted.", - success_path: namespace_project_tree_path(@project.namespace, @project, @target_branch), + success_path: -> { namespace_project_tree_path(@project.namespace, @project, @target_branch) }, failure_view: :show, failure_path: namespace_project_blob_path(@project.namespace, @project, @id)) end diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index e10d7992db7..cc67f688d51 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -51,23 +51,35 @@ class Projects::CommitController < Projects::ApplicationController def revert assign_change_commit_vars - return render_404 if @target_branch.blank? + return render_404 if @start_branch.blank? + + @target_branch = create_new_branch? ? @commit.revert_branch_name : @start_branch + + @mr_target_branch = @start_branch create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully reverted.", - success_path: successful_change_path, failure_path: failed_change_path) + success_path: -> { successful_change_path }, failure_path: failed_change_path) end def cherry_pick assign_change_commit_vars - return render_404 if @target_branch.blank? + return render_404 if @start_branch.blank? + + @target_branch = create_new_branch? ? @commit.cherry_pick_branch_name : @start_branch + + @mr_target_branch = @start_branch create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully cherry-picked.", - success_path: successful_change_path, failure_path: failed_change_path) + success_path: -> { successful_change_path }, failure_path: failed_change_path) end private + def create_new_branch? + params[:create_merge_request].present? || !can?(current_user, :push_code, @project) + end + def successful_change_path referenced_merge_request_url || namespace_project_commits_url(@project.namespace, @project, @target_branch) end @@ -78,7 +90,7 @@ class Projects::CommitController < Projects::ApplicationController def referenced_merge_request_url if merge_request = @commit.merged_merge_request(current_user) - namespace_project_merge_request_url(@project.namespace, @project, merge_request) + namespace_project_merge_request_url(merge_request.target_project.namespace, merge_request.target_project, merge_request) end end @@ -94,7 +106,7 @@ class Projects::CommitController < Projects::ApplicationController @diffs = commit.diffs(opts) @notes_count = commit.notes.count - + @environment = EnvironmentsFinder.new(@project, current_user, commit: @commit).execute.last end @@ -118,11 +130,7 @@ class Projects::CommitController < Projects::ApplicationController end def assign_change_commit_vars - @commit = project.commit(params[:id]) - @target_branch = params[:target_branch] - @commit_params = { - commit: @commit, - create_merge_request: params[:create_merge_request].present? || different_project? - } + @start_branch = params[:start_branch] + @commit_params = { commit: @commit } end end diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb index 923e7340e69..43fc0c39801 100644 --- a/app/controllers/projects/graphs_controller.rb +++ b/app/controllers/projects/graphs_controller.rb @@ -17,6 +17,25 @@ class Projects::GraphsController < Projects::ApplicationController end def commits + redirect_to action: 'charts' + end + + def languages + redirect_to action: 'charts' + end + + def charts + get_commits + get_languages + end + + def ci + redirect_to charts_namespace_project_pipelines_path(@project.namespace, @project) + end + + private + + def get_commits @commits = @project.repository.commits(@ref, limit: 2000, skip_merges: true) @commits_graph = Gitlab::Graphs::Commits.new(@commits) @commits_per_week_days = @commits_graph.commits_per_week_days @@ -24,15 +43,7 @@ class Projects::GraphsController < Projects::ApplicationController @commits_per_month = @commits_graph.commits_per_month end - def ci - @charts = {} - @charts[:week] = Ci::Charts::WeekChart.new(project) - @charts[:month] = Ci::Charts::MonthChart.new(project) - @charts[:year] = Ci::Charts::YearChart.new(project) - @charts[:build_times] = Ci::Charts::BuildTime.new(project) - end - - def languages + def get_languages @languages = Linguist::Repository.new(@repository.rugged, @repository.rugged.head.target_id).languages total = @languages.map(&:last).sum @@ -52,8 +63,6 @@ class Projects::GraphsController < Projects::ApplicationController end end - private - def fetch_graph @commits = @project.repository.commits(@ref, limit: 6000, skip_merges: true) @log = [] diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 53f30a24312..82f9b6e06db 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -10,11 +10,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :module_enabled before_action :merge_request, only: [ :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge, :merge_check, - :ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues + :ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues ] before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines] before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines] - before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check] + before_action :define_widget_vars, only: [:merge, :cancel_merge_when_pipeline_succeeds, :merge_check] before_action :define_commit_vars, only: [:diffs] before_action :define_diff_comment_vars, only: [:diffs] before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :conflict_for_path, :pipelines] @@ -245,9 +245,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController format.json do define_pipelines_vars - render json: PipelineSerializer + render json: { + pipelines: PipelineSerializer .new(project: @project, user: @current_user) .represent(@pipelines) + } end end end @@ -322,12 +324,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController def merge_check @merge_request.check_if_can_be_merged + @pipelines = @merge_request.all_pipelines render partial: "projects/merge_requests/widget/show.html.haml", layout: false end - def cancel_merge_when_build_succeeds - unless @merge_request.can_cancel_merge_when_build_succeeds?(current_user) + def cancel_merge_when_pipeline_succeeds + unless @merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user) return access_denied! end @@ -339,9 +342,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController def merge return access_denied! unless @merge_request.can_be_merged_by?(current_user) - # Disable the CI check if merge_when_build_succeeds is enabled since we have + # Disable the CI check if merge_when_pipeline_succeeds is enabled since we have # to wait until CI completes to know - unless @merge_request.mergeable?(skip_ci_check: merge_when_build_succeeds_active?) + unless @merge_request.mergeable?(skip_ci_check: merge_when_pipeline_succeeds_active?) @status = :failed return end @@ -353,7 +356,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request.update(merge_error: nil) - if params[:merge_when_build_succeeds].present? + if params[:merge_when_pipeline_succeeds].present? unless @merge_request.head_pipeline @status = :failed return @@ -364,7 +367,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController .new(@project, current_user, merge_params) .execute(@merge_request) - @status = :merge_when_build_succeeds + @status = :merge_when_pipeline_succeeds elsif @merge_request.head_pipeline.success? # This can be triggered when a user clicks the auto merge button while # the tests finish at about the same time @@ -381,8 +384,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController def merge_widget_refresh @status = - if merge_request.merge_when_build_succeeds - :merge_when_build_succeeds + if merge_request.merge_when_pipeline_succeeds + :merge_when_pipeline_succeeds else # Only MRs that can be merged end in this action # MR can be already picked up for merge / merged already or can be waiting for worker to be picked up @@ -444,6 +447,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def ci_status pipeline = @merge_request.head_pipeline + @pipelines = @merge_request.all_pipelines if pipeline status = pipeline.status @@ -462,7 +466,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController sha: (merge_request.diff_head_commit.short_id if merge_request.diff_head_sha), status: status, coverage: coverage, - pipeline: pipeline.try(:id) + pipeline: pipeline.try(:id), + has_ci: @merge_request.has_ci? } render json: response @@ -672,8 +677,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request.ensure_ref_fetched end - def merge_when_build_succeeds_active? - params[:merge_when_build_succeeds].present? && + def merge_when_pipeline_succeeds_active? + params[:merge_when_pipeline_succeeds].present? && @merge_request.head_pipeline && @merge_request.head_pipeline.active? end diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index b033f7b5ea9..5cf3a7f593b 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -148,17 +148,10 @@ class Projects::NotesController < Projects::ApplicationController def note_json(note) attrs = { - award: false, id: note.id } - if note.is_a?(AwardEmoji) - attrs.merge!( - valid: note.valid?, - award: true, - name: note.name - ) - elsif note.persisted? + if note.persisted? Banzai::NoteRenderer.render([note], @project, current_user) attrs.merge!( @@ -198,7 +191,7 @@ class Projects::NotesController < Projects::ApplicationController ) end - attrs[:commands_changes] = note.commands_changes unless attrs[:award] + attrs[:commands_changes] = note.commands_changes attrs end diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 8657bc4dfdc..718d9e86bea 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -1,9 +1,10 @@ class Projects::PipelinesController < Projects::ApplicationController - before_action :pipeline, except: [:index, :new, :create] + before_action :pipeline, except: [:index, :new, :create, :charts] before_action :commit, only: [:show, :builds] before_action :authorize_read_pipeline! before_action :authorize_create_pipeline!, only: [:new, :create] before_action :authorize_update_pipeline!, only: [:retry, :cancel] + before_action :builds_enabled, only: :charts def index @scope = params[:scope] @@ -92,6 +93,14 @@ class Projects::PipelinesController < Projects::ApplicationController redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project) end + def charts + @charts = {} + @charts[:week] = Ci::Charts::WeekChart.new(project) + @charts[:month] = Ci::Charts::MonthChart.new(project) + @charts[:year] = Ci::Charts::YearChart.new(project) + @charts[:build_times] = Ci::Charts::BuildTime.new(project) + end + private def create_params diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index acca821782c..3e2015b7d5e 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -314,7 +314,7 @@ class ProjectsController < Projects::ApplicationController :name, :namespace_id, :only_allow_merge_if_all_discussions_are_resolved, - :only_allow_merge_if_build_succeeds, + :only_allow_merge_if_pipeline_succeeds, :path, :public_builds, :request_access_enabled, diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb index db2817fadf6..1b4545e4a49 100644 --- a/app/controllers/root_controller.rb +++ b/app/controllers/root_controller.rb @@ -8,7 +8,9 @@ # `DashboardController#show`, which is the default. class RootController < Dashboard::ProjectsController skip_before_action :authenticate_user!, only: [:index] - before_action :redirect_to_custom_dashboard, only: [:index] + + before_action :redirect_unlogged_user, if: -> { current_user.nil? } + before_action :redirect_logged_user, if: -> { current_user.present? } def index super @@ -16,23 +18,38 @@ class RootController < Dashboard::ProjectsController private - def redirect_to_custom_dashboard - return redirect_to new_user_session_path unless current_user + def redirect_unlogged_user + if redirect_to_home_page_url? + redirect_to(current_application_settings.home_page_url) + else + redirect_to(new_user_session_path) + end + end + def redirect_logged_user case current_user.dashboard when 'stars' flash.keep - redirect_to starred_dashboard_projects_path + redirect_to(starred_dashboard_projects_path) when 'project_activity' - redirect_to activity_dashboard_path + redirect_to(activity_dashboard_path) when 'starred_project_activity' - redirect_to activity_dashboard_path(filter: 'starred') + redirect_to(activity_dashboard_path(filter: 'starred')) when 'groups' - redirect_to dashboard_groups_path + redirect_to(dashboard_groups_path) when 'todos' - redirect_to dashboard_todos_path - else - return + redirect_to(dashboard_todos_path) end end + + def redirect_to_home_page_url? + # If user is not signed-in and tries to access root_path - redirect him to landing page + # Don't redirect to the default URL to prevent endless redirections + return false unless current_application_settings.home_page_url.present? + + home_page_url = current_application_settings.home_page_url.chomp('/') + root_urls = [Gitlab.config.gitlab['url'].chomp('/'), root_url.chomp('/')] + + root_urls.exclude?(home_page_url) + end end diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb index ff937b5ebd2..5ac3e66bb1f 100644 --- a/app/helpers/builds_helper.rb +++ b/app/helpers/builds_helper.rb @@ -15,4 +15,11 @@ module BuildsHelper log_state: @build.trace_with_state[:state].to_s } end + + def build_failed_issue_options + { + title: "Build Failed ##{@build.id}", + description: namespace_project_build_url(@project.namespace, @project, @build) + } + end end diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index 195094730aa..0b30471f2ae 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -19,7 +19,7 @@ module ButtonHelper title = data[:title] || 'Copy to clipboard' data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data) content_tag :button, - icon('clipboard'), + icon('clipboard', 'aria-hidden': 'true'), class: "btn #{css_class}", data: data, type: :button, diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb index 2b1f3825adc..bbcb52f7eaf 100644 --- a/app/helpers/explore_helper.rb +++ b/app/helpers/explore_helper.rb @@ -9,12 +9,20 @@ module ExploreHelper } options = exist_opts.merge(options) - path = request.path - path << "?#{options.to_param}" - path + request_path_with_options(options) + end + + def filter_groups_path(options = {}) + request_path_with_options(options) end def explore_controller? controller.class.name.split("::").first == "Explore" end + + private + + def request_path_with_options(options = {}) + request.path + "?#{options.to_param}" + end end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 860a665ae26..c2b399041c6 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -1,6 +1,6 @@ module IssuablesHelper def sidebar_gutter_toggle_icon - sidebar_gutter_collapsed? ? icon('angle-double-left') : icon('angle-double-right') + sidebar_gutter_collapsed? ? icon('angle-double-left', { 'aria-hidden': 'true' }) : icon('angle-double-right', { 'aria-hidden': 'true' }) end def sidebar_gutter_collapsed_class diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 7d8505d704e..38be073c8dc 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -146,7 +146,7 @@ module MergeRequestsHelper def merge_params(merge_request) { - merge_when_build_succeeds: true, + merge_when_pipeline_succeeds: true, should_remove_source_branch: true, sha: merge_request.diff_head_sha }.merge(merge_params_ee(merge_request)) diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 729928ce1dd..7011e670cee 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -97,7 +97,7 @@ module MilestonesHelper def milestone_date_range(milestone) if milestone.start_date && milestone.due_date - "#{milestone.start_date.to_s(:medium)} - #{milestone.due_date.to_s(:medium)}" + "#{milestone.start_date.to_s(:medium)}–#{milestone.due_date.to_s(:medium)}" elsif milestone.due_date if milestone.due_date.past? "expired on #{milestone.due_date.to_s(:medium)}" diff --git a/app/helpers/rss_helper.rb b/app/helpers/rss_helper.rb new file mode 100644 index 00000000000..ea5d2932ef4 --- /dev/null +++ b/app/helpers/rss_helper.rb @@ -0,0 +1,5 @@ +module RssHelper + def rss_url_options + { format: :atom, private_token: current_user.try(:private_token) } + end +end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index dc36c754438..255e8c4ff78 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -179,6 +179,7 @@ class ApplicationSetting < ActiveRecord::Base default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_projects_limit: Settings.gitlab['default_projects_limit'], default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], + default_group_visibility: Settings.gitlab.default_projects_features['visibility_level'], disabled_oauth_sign_in_sources: [], domain_whitelist: Settings.gitlab['domain_whitelist'], gravatar_enabled: Settings.gravatar['enabled'], @@ -277,6 +278,22 @@ class ApplicationSetting < ActiveRecord::Base self.repository_storages = [value] end + def default_project_visibility=(level) + super(Gitlab::VisibilityLevel.level_value(level)) + end + + def default_snippet_visibility=(level) + super(Gitlab::VisibilityLevel.level_value(level)) + end + + def default_group_visibility=(level) + super(Gitlab::VisibilityLevel.level_value(level)) + end + + def restricted_visibility_levels=(levels) + super(levels.map { |level| Gitlab::VisibilityLevel.level_value(level) }) + end + # Choose one of the available repository storage options. Currently all have # equal weighting. def pick_repository_storage diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 77aba91f65c..f2989eff22d 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -55,15 +55,6 @@ module Ci pending.unstarted.order('created_at ASC').first end - def create_from(build) - new_build = build.dup - new_build.status = 'pending' - new_build.runner_id = nil - new_build.trigger_request_id = nil - new_build.token = nil - new_build.save - end - def retry(build, current_user) Ci::RetryBuildService .new(build.project, current_user) diff --git a/app/models/group.rb b/app/models/group.rb index 240a17f1dc1..7d23f655225 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -93,7 +93,7 @@ class Group < Namespace end def visibility_level_field - visibility_level + :visibility_level end def visibility_level_allowed_by_projects diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 7eb875f1ef5..0f7b8311588 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -91,17 +91,13 @@ class MergeRequest < ActiveRecord::Base around_transition do |merge_request, transition, block| Gitlab::Timeless.timeless(merge_request, &block) end - - after_transition unchecked: :cannot_be_merged do |merge_request, transition| - TodoService.new.merge_request_became_unmergeable(merge_request) - end end validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?] validates :source_branch, presence: true validates :target_project, presence: true validates :target_branch, presence: true - validates :merge_user, presence: true, if: :merge_when_build_succeeds?, unless: :importing? + validates :merge_user, presence: true, if: :merge_when_pipeline_succeeds?, unless: :importing? validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?] validate :validate_fork, unless: :closed_without_fork? @@ -440,7 +436,7 @@ class MergeRequest < ActiveRecord::Base true end - def can_cancel_merge_when_build_succeeds?(current_user) + def can_cancel_merge_when_pipeline_succeeds?(current_user) can_be_merged_by?(current_user) || self.author == current_user end @@ -648,10 +644,10 @@ class MergeRequest < ActiveRecord::Base message.join("\n\n") end - def reset_merge_when_build_succeeds - return unless merge_when_build_succeeds? + def reset_merge_when_pipeline_succeeds + return unless merge_when_pipeline_succeeds? - self.merge_when_build_succeeds = false + self.merge_when_pipeline_succeeds = false self.merge_user = nil if merge_params merge_params.delete('should_remove_source_branch') @@ -688,7 +684,10 @@ class MergeRequest < ActiveRecord::Base end def has_ci? - source_project.try(:ci_service) && commits.any? + has_ci_integration = source_project.try(:ci_service) + uses_gitlab_ci = all_pipelines.any? + + (has_ci_integration || uses_gitlab_ci) && commits.any? end def branch_missing? @@ -710,7 +709,7 @@ class MergeRequest < ActiveRecord::Base end def mergeable_ci_state? - return true unless project.only_allow_merge_if_build_succeeds? + return true unless project.only_allow_merge_if_pipeline_succeeds? !head_pipeline || head_pipeline.success? || head_pipeline.skipped? end diff --git a/app/models/note.rb b/app/models/note.rb index d6d5396afa5..4c97e4a986c 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -231,10 +231,6 @@ class Note < ActiveRecord::Base note =~ /\A#{Banzai::Filter::EmojiFilter.emoji_pattern}\s?\Z/ end - def award_emoji_name - note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1] - end - def to_ability_name for_personal_snippet? ? 'personal_snippet' : noteable_type.underscore end diff --git a/app/models/project.rb b/app/models/project.rb index e06fc30dc8a..1ac4a178a9b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -19,7 +19,7 @@ class Project < ActiveRecord::Base extend Gitlab::ConfigHelper - class BoardLimitExceeded < StandardError; end + BoardLimitExceeded = Class.new(StandardError) NUMBER_OF_PERMITTED_BOARDS = 1 UNKNOWN_IMPORT_URL = 'http://unknown.git'.freeze @@ -113,6 +113,7 @@ class Project < ActiveRecord::Base has_one :gitlab_issue_tracker_service, dependent: :destroy, inverse_of: :project has_one :external_wiki_service, dependent: :destroy has_one :kubernetes_service, dependent: :destroy, inverse_of: :project + has_one :mock_ci_service, dependent: :destroy has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" has_one :forked_from_project, through: :forked_project_link @@ -334,7 +335,7 @@ class Project < ActiveRecord::Base end def search_by_visibility(level) - where(visibility_level: Gitlab::VisibilityLevel.const_get(level.upcase)) + where(visibility_level: Gitlab::VisibilityLevel.string_options[level]) end def search_by_title(query) @@ -1003,7 +1004,7 @@ class Project < ActiveRecord::Base end def visibility_level_field - visibility_level + :visibility_level end def archive! diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb index 5cb6b0c527d..ac1e9ab2b0b 100644 --- a/app/models/project_group_link.rb +++ b/app/models/project_group_link.rb @@ -33,8 +33,15 @@ class ProjectGroupLink < ActiveRecord::Base private def different_group - if self.group && self.project && self.project.group == self.group - errors.add(:base, "Project cannot be shared with the project it is in.") + return unless self.group && self.project + + project_group = self.project.group + return unless project_group + + group_ids = project_group.ancestors.map(&:id).push(project_group.id) + + if group_ids.include?(self.group.id) + errors.add(:base, "Project cannot be shared with the group it is in or one of its ancestors.") end end diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index 9819e723fe8..f2e1c906dac 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -94,7 +94,12 @@ class KubernetesService < DeploymentService { key: 'KUBE_TOKEN', value: token, public: false }, { key: 'KUBE_NAMESPACE', value: namespace, public: true } ] - variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true } if ca_pem.present? + + if ca_pem.present? + variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true } + variables << { key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true } + end + variables end diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 9891f5edf41..539b31780b3 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -7,7 +7,7 @@ class ProjectWiki 'AsciiDoc' => :asciidoc }.freeze unless defined?(MARKUPS) - class CouldNotCreateWikiError < StandardError; end + CouldNotCreateWikiError = Class.new(StandardError) # Returns a string describing what went wrong after # an operation fails. diff --git a/app/models/repository.rb b/app/models/repository.rb index 0dbf246c3a4..e7cc8d6e083 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -6,6 +6,7 @@ class Repository attr_accessor :path_with_namespace, :project CommitError = Class.new(StandardError) + CreateTreeError = Class.new(StandardError) # Methods that cache data from the Git repository. # @@ -862,17 +863,18 @@ class Repository end def revert( - user, commit, branch_name, revert_tree_id = nil, + user, commit, branch_name, start_branch_name: nil, start_project: project) - revert_tree_id ||= check_revert_content(commit, branch_name) - - return false unless revert_tree_id - GitOperationService.new(user, self).with_branch( branch_name, start_branch_name: start_branch_name, start_project: start_project) do |start_commit| + revert_tree_id = check_revert_content(commit, start_commit.sha) + unless revert_tree_id + raise Repository::CreateTreeError.new('Failed to revert commit') + end + committer = user_to_committer(user) Rugged::Commit.create(rugged, @@ -885,17 +887,18 @@ class Repository end def cherry_pick( - user, commit, branch_name, cherry_pick_tree_id = nil, + user, commit, branch_name, start_branch_name: nil, start_project: project) - cherry_pick_tree_id ||= check_cherry_pick_content(commit, branch_name) - - return false unless cherry_pick_tree_id - GitOperationService.new(user, self).with_branch( branch_name, start_branch_name: start_branch_name, start_project: start_project) do |start_commit| + cherry_pick_tree_id = check_cherry_pick_content(commit, start_commit.sha) + unless cherry_pick_tree_id + raise Repository::CreateTreeError.new('Failed to cherry-pick commit') + end + committer = user_to_committer(user) Rugged::Commit.create(rugged, @@ -919,9 +922,8 @@ class Repository end end - def check_revert_content(target_commit, branch_name) - source_sha = commit(branch_name).sha - args = [target_commit.sha, source_sha] + def check_revert_content(target_commit, source_sha) + args = [target_commit.sha, source_sha] args << { mainline: 1 } if target_commit.merge_commit? revert_index = rugged.revert_commit(*args) @@ -933,9 +935,8 @@ class Repository tree_id end - def check_cherry_pick_content(target_commit, branch_name) - source_sha = commit(branch_name).sha - args = [target_commit.sha, source_sha] + def check_cherry_pick_content(target_commit, source_sha) + args = [target_commit.sha, source_sha] args << 1 if target_commit.merge_commit? cherry_pick_index = rugged.cherrypick_commit(*args) @@ -995,6 +996,8 @@ class Repository end def with_repo_branch_commit(start_repository, start_branch_name) + return yield(nil) if start_repository.empty_repo? + branch_name_or_sha = if start_repository == self start_branch_name diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 2665a7249a3..dbd564e5e7d 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -120,7 +120,7 @@ class Snippet < ActiveRecord::Base end def visibility_level_field - visibility_level + :visibility_level end def no_highlighting? diff --git a/app/models/user.rb b/app/models/user.rb index 40264401b53..8443594c055 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -346,7 +346,11 @@ class User < ActiveRecord::Base # Return (create if necessary) the ghost user. The ghost user # owns records previously belonging to deleted users. def ghost - User.find_by_ghost(true) || create_ghost_user + unique_internal(where(ghost: true), 'ghost', 'ghost%s@example.com') do |u| + u.bio = 'This is a "Ghost User", created to hold all issues authored by users that have since been deleted. This user cannot be removed.' + u.state = :blocked + u.name = 'Ghost User' + end end end @@ -474,7 +478,7 @@ class User < ActiveRecord::Base Group.member_descendants(id) end - def nested_projects + def nested_groups_projects Project.joins(:namespace).where('namespaces.parent_id IS NOT NULL'). member_descendants(id) end @@ -1017,10 +1021,14 @@ class User < ActiveRecord::Base end end - def self.create_ghost_user - # Since we only want a single ghost user in an instance, we use an + def self.unique_internal(scope, username, email_pattern, &b) + scope.first || create_unique_internal(scope, username, email_pattern, &b) + end + + def self.create_unique_internal(scope, username, email_pattern, &creation_block) + # Since we only want a single one of these in an instance, we use an # exclusive lease to ensure than this block is never run concurrently. - lease_key = "ghost_user_creation" + lease_key = "user:unique_internal:#{username}" lease = Gitlab::ExclusiveLease.new(lease_key, timeout: 1.minute.to_i) until uuid = lease.try_obtain @@ -1029,25 +1037,25 @@ class User < ActiveRecord::Base sleep(1) end - # Recheck if a ghost user is already present. One might have been + # Recheck if the user is already present. One might have been # added between the time we last checked (first line of this method) # and the time we acquired the lock. - ghost_user = User.find_by_ghost(true) - return ghost_user if ghost_user.present? + existing_user = uncached { scope.first } + return existing_user if existing_user.present? uniquify = Uniquify.new - username = uniquify.string("ghost") { |s| User.find_by_username(s) } + username = uniquify.string(username) { |s| User.find_by_username(s) } - email = uniquify.string(-> (n) { "ghost#{n}@example.com" }) do |s| + email = uniquify.string(-> (n) { Kernel.sprintf(email_pattern, n) }) do |s| User.find_by_email(s) end - bio = 'This is a "Ghost User", created to hold all issues authored by users that have since been deleted. This user cannot be removed.' - - User.create( - username: username, password: Devise.friendly_token, bio: bio, - email: email, name: "Ghost User", state: :blocked, ghost: true + scope.create( + username: username, + password: Devise.friendly_token, + email: email, + &creation_block ) ensure Gitlab::ExclusiveLease.cancel(lease_key, uuid) diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index 0be6e113655..4cc21696eb6 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -33,8 +33,6 @@ class GroupPolicy < BasePolicy if globally_viewable && @subject.request_access_enabled && !member can! :request_access end - - additional_rules!(master) end def can_read_group? @@ -45,8 +43,4 @@ class GroupPolicy < BasePolicy GroupProjectsFinder.new(@subject).execute(@user).any? end - - def additional_rules!(master) - # This is meant to be overriden in EE - end end diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb index 7445298c714..5f80ab397a9 100644 --- a/app/serializers/merge_request_entity.rb +++ b/app/serializers/merge_request_entity.rb @@ -6,7 +6,7 @@ class MergeRequestEntity < IssuableEntity expose :merge_params expose :merge_status expose :merge_user_id - expose :merge_when_build_succeeds + expose :merge_when_pipeline_succeeds expose :source_branch expose :source_project_id expose :target_branch diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb index 2bc6cf3266e..ab2d3d5a3ec 100644 --- a/app/serializers/pipeline_serializer.rb +++ b/app/serializers/pipeline_serializer.rb @@ -1,5 +1,5 @@ class PipelineSerializer < BaseSerializer - class InvalidResourceError < StandardError; end + InvalidResourceError = Class.new(StandardError) entity PipelineEntity diff --git a/app/services/access_token_validation_service.rb b/app/services/access_token_validation_service.rb index ddaaed90e5b..b2a543daa00 100644 --- a/app/services/access_token_validation_service.rb +++ b/app/services/access_token_validation_service.rb @@ -1,10 +1,16 @@ -AccessTokenValidationService = Struct.new(:token) do +class AccessTokenValidationService # Results: VALID = :valid EXPIRED = :expired REVOKED = :revoked INSUFFICIENT_SCOPE = :insufficient_scope + attr_reader :token + + def initialize(token) + @token = token + end + def validate(scopes: []) if token.expired? return EXPIRED diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb deleted file mode 100644 index 240ddabec36..00000000000 --- a/app/services/ci/image_for_build_service.rb +++ /dev/null @@ -1,25 +0,0 @@ -module Ci - class ImageForBuildService - def execute(project, opts) - ref = opts[:ref] - sha = opts[:sha] || ref_sha(project, ref) - pipelines = project.pipelines.where(sha: sha) - - image_name = image_for_status(pipelines.latest_status(ref)) - image_path = Rails.root.join('public/ci', image_name) - - OpenStruct.new(path: image_path, name: image_name) - end - - private - - def ref_sha(project, ref) - project.commit(ref).try(:sha) if ref - end - - def image_for_status(status) - status ||= 'unknown' - 'build-' + status + ".svg" - end - end -end diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb index 6f03bf2be13..5b52a0425de 100644 --- a/app/services/ci/register_build_service.rb +++ b/app/services/ci/register_build_service.rb @@ -20,21 +20,33 @@ module Ci builds_for_specific_runner end - build = builds.find do |build| - runner.can_pick?(build) - end + valid = true - if build - # In case when 2 runners try to assign the same build, second runner will be declined - # with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method. - build.runner_id = runner.id - build.run! - end + builds.find do |build| + next unless runner.can_pick?(build) + + begin + # In case when 2 runners try to assign the same build, second runner will be declined + # with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method. + build.runner_id = runner.id + build.run! - Result.new(build, true) + return Result.new(build, true) + rescue StateMachines::InvalidTransition, ActiveRecord::StaleObjectError + # We are looping to find another build that is not conflicting + # It also indicates that this build can be picked and passed to runner. + # If we don't do it, basically a bunch of runners would be competing for a build + # and thus we will generate a lot of 409. This will increase + # the number of generated requests, also will reduce significantly + # how many builds can be picked by runner in a unit of time. + # In case we hit the concurrency-access lock, + # we still have to return 409 in the end, + # to make sure that this is properly handled by runner. + valid = false + end + end - rescue StateMachines::InvalidTransition, ActiveRecord::StaleObjectError - Result.new(build, false) + Result.new(nil, valid) end private diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb index 38ef323f6e5..89da05b72bb 100644 --- a/app/services/ci/retry_build_service.rb +++ b/app/services/ci/retry_build_service.rb @@ -1,18 +1,9 @@ module Ci class RetryBuildService < ::BaseService - CLONE_ATTRIBUTES = %i[pipeline project ref tag options commands name - allow_failure stage stage_idx trigger_request - yaml_variables when environment coverage_regex] - .freeze - - REJECT_ATTRIBUTES = %i[id status user token coverage trace runner - artifacts_expire_at artifacts_file - artifacts_metadata artifacts_size - created_at updated_at started_at finished_at - queued_at erased_by erased_at].freeze - - IGNORE_ATTRIBUTES = %i[type lock_version gl_project_id target_url - deploy job_id description].freeze + CLONE_ACCESSORS = %i[pipeline project ref tag options commands name + allow_failure stage stage_idx trigger_request + yaml_variables when environment coverage_regex + description tag_list].freeze def execute(build) reprocess(build).tap do |new_build| @@ -31,7 +22,7 @@ module Ci raise Gitlab::Access::AccessDeniedError end - attributes = CLONE_ATTRIBUTES.map do |attribute| + attributes = CLONE_ACCESSORS.map do |attribute| [attribute, build.send(attribute)] end diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb index 25e22f14e60..1297a792259 100644 --- a/app/services/commits/change_service.rb +++ b/app/services/commits/change_service.rb @@ -1,16 +1,16 @@ module Commits class ChangeService < ::BaseService - class ValidationError < StandardError; end - class ChangeError < StandardError; end + ValidationError = Class.new(StandardError) + ChangeError = Class.new(StandardError) def execute @start_project = params[:start_project] || @project @start_branch = params[:start_branch] @target_branch = params[:target_branch] @commit = params[:commit] - @create_merge_request = params[:create_merge_request].present? - check_push_permissions unless @create_merge_request + check_push_permissions + commit rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError, ValidationError, ChangeError => ex @@ -26,34 +26,21 @@ module Commits def commit_change(action) raise NotImplementedError unless repository.respond_to?(action) - if @create_merge_request - into = @commit.public_send("#{action}_branch_name") - tree_branch = @start_branch - else - into = tree_branch = @target_branch - end - - tree_id = repository.public_send( - "check_#{action}_content", @commit, tree_branch) - - if tree_id - validate_target_branch(into) if @create_merge_request + validate_target_branch if different_branch? - repository.public_send( - action, - current_user, - @commit, - into, - tree_id, - start_project: @start_project, - start_branch_name: @start_branch) + repository.public_send( + action, + current_user, + @commit, + @target_branch, + start_project: @start_project, + start_branch_name: @start_branch) - success - else - error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically. + success + rescue Repository::CreateTreeError + error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically. A #{action.to_s.dasherize} may have already been performed with this #{@commit.change_type_title(current_user)}, or a more recent commit may have updated some of its content." - raise ChangeError, error_msg - end + raise ChangeError, error_msg end def check_push_permissions @@ -66,16 +53,17 @@ module Commits true end - def validate_target_branch(new_branch) - # Temporary branch exists and contains the change commit - return if repository.find_branch(new_branch) - + def validate_target_branch result = ValidateNewBranchService.new(@project, current_user) - .execute(new_branch) + .execute(@target_branch) if result[:status] == :error raise ChangeError, "There was an error creating the source branch: #{result[:message]}" end end + + def different_branch? + @start_branch != @target_branch || @start_project != @project + end end end diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index 0a25f56d24c..c8a60422bf4 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -1,6 +1,6 @@ module Files class BaseService < ::BaseService - class ValidationError < StandardError; end + ValidationError = Class.new(StandardError) def execute @start_project = params[:start_project] || @project @@ -58,16 +58,12 @@ module Files raise_error("You are not allowed to push into this branch") end - unless project.empty_repo? - unless @start_project.repository.branch_exists?(@start_branch) - raise_error('You can only create or edit files when you are on a branch') - end + if !@start_project.empty_repo? && !@start_project.repository.branch_exists?(@start_branch) + raise ValidationError, 'You can only create or edit files when you are on a branch' + end - if different_branch? - if repository.branch_exists?(@target_branch) - raise_error('Branch with such name already exists. You need to switch to this branch in order to make changes') - end - end + if !project.empty_repo? && different_branch? && repository.branch_exists?(@branch_name) + raise ValidationError, "A branch called #{@branch_name} already exists. Switch to that branch in order to make changes" end end diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb index 0609c6219e7..700f9f4f6f0 100644 --- a/app/services/files/multi_service.rb +++ b/app/services/files/multi_service.rb @@ -1,6 +1,6 @@ module Files class MultiService < Files::BaseService - class FileChangedError < StandardError; end + FileChangedError = Class.new(StandardError) ACTIONS = %w[create update delete move].freeze diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb index 54e1aaf3f67..fbbab97632e 100644 --- a/app/services/files/update_service.rb +++ b/app/services/files/update_service.rb @@ -1,6 +1,6 @@ module Files class UpdateService < Files::BaseService - class FileChangedError < StandardError; end + FileChangedError = Class.new(StandardError) def commit repository.update_file(current_user, @file_path, @file_content, diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb index 27bcc047601..ed6ea638235 100644 --- a/app/services/git_operation_service.rb +++ b/app/services/git_operation_service.rb @@ -56,12 +56,16 @@ class GitOperationService start_project: repository.project, &block) - check_with_branch_arguments!( - branch_name, start_branch_name, start_project) + start_repository = start_project.repository + start_branch_name = nil if start_repository.empty_repo? + + if start_branch_name && !start_repository.branch_exists?(start_branch_name) + raise ArgumentError, "Cannot find branch #{start_branch_name} in #{start_repository.path_with_namespace}" + end update_branch_with_hooks(branch_name) do repository.with_repo_branch_commit( - start_project.repository, + start_repository, start_branch_name || branch_name, &block) end @@ -149,31 +153,4 @@ class GitOperationService repository.raw_repository.autocrlf = :input end end - - def check_with_branch_arguments!( - branch_name, start_branch_name, start_project) - return if repository.branch_exists?(branch_name) - - if repository.project != start_project - unless start_branch_name - raise ArgumentError, - 'Should also pass :start_branch_name if' + - ' :start_project is different from current project' - end - - unless start_project.repository.branch_exists?(start_branch_name) - raise ArgumentError, - "Cannot find branch #{branch_name} nor" \ - " #{start_branch_name} from" \ - " #{start_project.path_with_namespace}" - end - elsif start_branch_name - unless repository.branch_exists?(start_branch_name) - raise ArgumentError, - "Cannot find branch #{branch_name} nor" \ - " #{start_branch_name} from" \ - " #{repository.project.path_with_namespace}" - end - end - end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 9500faf2862..b618c3e038e 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -203,6 +203,7 @@ class IssuableBaseService < BaseService change_state(issuable) change_subscription(issuable) change_todo(issuable) + toggle_award(issuable) filter_params(issuable) old_labels = issuable.labels.to_a old_mentioned_users = issuable.mentioned_users.to_a @@ -263,6 +264,14 @@ class IssuableBaseService < BaseService end end + def toggle_award(issuable) + award = params.delete(:emoji_award) + if award + todo_service.new_award_emoji(issuable, current_user) + issuable.toggle_award_emoji(award, current_user) + end + end + def has_changes?(issuable, old_labels: []) valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch] diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index a2a5f57d069..711f4035c55 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -1,6 +1,6 @@ module Issues class MoveService < Issues::BaseService - class MoveError < StandardError; end + MoveError = Class.new(StandardError) def execute(issue, new_project) @old_issue = issue diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 3da1b657a41..fac3ac7a4c7 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -6,6 +6,8 @@ module MergeRequests # Executed when you do merge via GitLab UI # class MergeService < MergeRequests::BaseService + MergeError = Class.new(StandardError) + attr_reader :merge_request, :source def execute(merge_request) @@ -27,6 +29,8 @@ module MergeRequests success end end + rescue MergeError => e + log_merge_error(e.message, save_message_on_model: true) end private @@ -42,19 +46,13 @@ module MergeRequests commit_id = repository.merge(current_user, source, merge_request, options) - if commit_id - merge_request.update(merge_commit_sha: commit_id) - else - log_merge_error('Conflicts detected during merge', save_message_on_model: true) - false - end + raise MergeError, 'Conflicts detected during merge' unless commit_id + + merge_request.update(merge_commit_sha: commit_id) rescue GitHooksService::PreReceiveError => e - log_merge_error(e.message, save_message_on_model: true) - false + raise MergeError, e.message rescue StandardError => e - merge_request.update(merge_error: "Something went wrong during merge: #{e.message}") - log_merge_error(e.message) - false + raise MergeError, "Something went wrong during merge: #{e.message}" ensure merge_request.update(in_progress_merge_commit_sha: nil) end diff --git a/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb index 5616edf8b4a..aed5287940e 100644 --- a/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb +++ b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb @@ -1,18 +1,18 @@ module MergeRequests class MergeWhenPipelineSucceedsService < MergeRequests::BaseService - # Marks the passed `merge_request` to be merged when the build succeeds or + # Marks the passed `merge_request` to be merged when the pipeline succeeds or # updates the params for the automatic merge def execute(merge_request) merge_request.merge_params.merge!(params) # The service is also called when the merge params are updated. - already_approved = merge_request.merge_when_build_succeeds? + already_approved = merge_request.merge_when_pipeline_succeeds? unless already_approved - merge_request.merge_when_build_succeeds = true - merge_request.merge_user = @current_user + merge_request.merge_when_pipeline_succeeds = true + merge_request.merge_user = @current_user - SystemNoteService.merge_when_build_succeeds(merge_request, @project, @current_user, merge_request.diff_head_commit) + SystemNoteService.merge_when_pipeline_succeeds(merge_request, @project, @current_user, merge_request.diff_head_commit) end merge_request.save @@ -23,8 +23,12 @@ module MergeRequests return unless pipeline.success? pipeline_merge_requests(pipeline) do |merge_request| - next unless merge_request.merge_when_build_succeeds? - next unless merge_request.mergeable? + next unless merge_request.merge_when_pipeline_succeeds? + + unless merge_request.mergeable? + todo_service.merge_request_became_unmergeable(merge_request) + next + end MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params) end @@ -32,9 +36,9 @@ module MergeRequests # Cancels the automatic merge def cancel(merge_request) - if merge_request.merge_when_build_succeeds? && merge_request.open? - merge_request.reset_merge_when_build_succeeds - SystemNoteService.cancel_merge_when_build_succeeds(merge_request, @project, @current_user) + if merge_request.merge_when_pipeline_succeeds? && merge_request.open? + merge_request.reset_merge_when_pipeline_succeeds + SystemNoteService.cancel_merge_when_pipeline_succeeds(merge_request, @project, @current_user) success else diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 581d18032e6..1131d6f4913 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -11,7 +11,7 @@ module MergeRequests # empty diff during a manual merge close_merge_requests reload_merge_requests - reset_merge_when_build_succeeds + reset_merge_when_pipeline_succeeds mark_pending_todos_done cache_merge_requests_closing_issues @@ -78,8 +78,8 @@ module MergeRequests end end - def reset_merge_when_build_succeeds - merge_requests_for_source_branch.each(&:reset_merge_when_build_succeeds) + def reset_merge_when_pipeline_succeeds + merge_requests_for_source_branch.each(&:reset_merge_when_pipeline_succeeds) end def mark_pending_todos_done diff --git a/app/services/merge_requests/resolve_service.rb b/app/services/merge_requests/resolve_service.rb index d22a1d3e0ad..82cd89d9a0b 100644 --- a/app/services/merge_requests/resolve_service.rb +++ b/app/services/merge_requests/resolve_service.rb @@ -1,7 +1,6 @@ module MergeRequests class ResolveService < MergeRequests::BaseService - class MissingFiles < Gitlab::Conflict::ResolutionError - end + MissingFiles = Class.new(Gitlab::Conflict::ResolutionError) attr_accessor :conflicts, :rugged, :merge_index, :merge_request diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index b4f8b33d564..61d66a26932 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -8,14 +8,6 @@ module Notes note.author = current_user note.system = false - if note.award_emoji? - noteable = note.noteable - if noteable.user_can_award?(current_user, note.award_emoji_name) - todo_service.new_award_emoji(noteable, current_user) - return noteable.create_award_emoji(note.award_emoji_name, current_user) - end - end - # We execute commands (extracted from `params[:note]`) on the noteable # **before** we save the note because if the note consists of commands # only, there is no need be create a note! @@ -48,7 +40,7 @@ module Notes note.errors.add(:commands_only, 'Commands applied') end - note.commands_changes = command_params.keys + note.commands_changes = command_params end note diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 3734e3c4253..fbad85d310e 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -135,7 +135,7 @@ class NotificationService merge_request.target_project, current_user, :merged_merge_request_email, - skip_current_user: !merge_request.merge_when_build_succeeds? + skip_current_user: !merge_request.merge_when_pipeline_succeeds? ) end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 6dc3d8c2416..fbdaa455651 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -12,7 +12,7 @@ module Projects @project = Project.new(params) # Make sure that the user is allowed to use the specified visibility level - unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level]) + unless Gitlab::VisibilityLevel.allowed_for?(current_user, @project.visibility_level) deny_visibility_level(@project) return @project end diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 2e06826c311..a7142d5950e 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -2,7 +2,7 @@ module Projects class DestroyService < BaseService include Gitlab::ShellAdapter - class DestroyError < StandardError; end + DestroyError = Class.new(StandardError) DELETED_FLAG = '+deleted'.freeze diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index cd230528743..1c5a549feb9 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -2,7 +2,7 @@ module Projects class ImportService < BaseService include Gitlab::ShellAdapter - class Error < StandardError; end + Error = Class.new(StandardError) def execute add_repository_to_project unless project.gitlab_project_import? diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 20dfbddc823..da6e6acd4a7 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -9,7 +9,7 @@ module Projects class TransferService < BaseService include Gitlab::ShellAdapter - class TransferError < StandardError; end + TransferError = Class.new(StandardError) def execute(new_namespace) if allowed_transfer?(current_user, project, new_namespace) diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb index 3e0a85cf059..595653ea58a 100644 --- a/app/services/slash_commands/interpret_service.rb +++ b/app/services/slash_commands/interpret_service.rb @@ -59,7 +59,7 @@ module SlashCommands @updates[:state_event] = 'reopen' end - desc 'Merge (when build succeeds)' + desc 'Merge (when the pipeline succeeds)' condition do last_diff_sha = params && params[:merge_request_diff_head_sha] issuable.is_a?(MergeRequest) && @@ -255,6 +255,18 @@ module SlashCommands @updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip' end + desc 'Toggle emoji reward' + params ':emoji:' + condition do + issuable.persisted? + end + command :award do |emoji| + name = award_emoji_name(emoji) + if name && issuable.user_can_award?(current_user, name) + @updates[:emoji_award] = name + end + end + desc 'Set time estimate' params '<1w 3d 2h 14m>' condition do @@ -329,5 +341,10 @@ module SlashCommands ext.references(type) end + + def award_emoji_name(emoji) + match = emoji.match(Banzai::Filter::EmojiFilter.emoji_pattern) + match[1] if match + end end end diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index 9b6dd013e3a..868fa7b3f21 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -84,7 +84,7 @@ class SystemHooksService project_id: model.id, owner_name: owner.name, owner_email: owner.respond_to?(:email) ? owner.email : "", - project_visibility: Project.visibility_levels.key(model.visibility_level_field).downcase + project_visibility: Project.visibility_levels.key(model.visibility_level_value).downcase } end @@ -101,7 +101,7 @@ class SystemHooksService user_email: model.user.email, user_id: model.user.id, access_level: model.human_access, - project_visibility: Project.visibility_levels.key(project.visibility_level_field).downcase + project_visibility: Project.visibility_levels.key(project.visibility_level_value).downcase } end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 55b548a12f9..8e02fe3741a 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -187,14 +187,14 @@ module SystemNoteService end # Called when 'merge when pipeline succeeds' is executed - def merge_when_build_succeeds(noteable, project, author, last_commit) + def merge_when_pipeline_succeeds(noteable, project, author, last_commit) body = "enabled an automatic merge when the pipeline for #{last_commit.to_reference(project)} succeeds" create_note(noteable: noteable, project: project, author: author, note: body) end # Called when 'merge when pipeline succeeds' is canceled - def cancel_merge_when_build_succeeds(noteable, project, author) + def cancel_merge_when_pipeline_succeeds(noteable, project, author) body = 'canceled the automatic merge' create_note(noteable: noteable, project: project, author: author, note: body) @@ -385,7 +385,6 @@ module SystemNoteService # Returns Boolean def cross_reference_disallowed?(noteable, mentioner) return true if noteable.is_a?(ExternalIssue) && !noteable.project.jira_tracker_active? - return true if noteable.is_a?(Issuable) && (noteable.try(:closed?) || noteable.try(:merged?)) return false unless mentioner.is_a?(MergeRequest) return false unless noteable.is_a?(Commit) diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index ad86b4f9f42..8787a1c93a9 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -103,7 +103,7 @@ class TodoService # def merge_request_build_failed(merge_request) create_build_failed_todo(merge_request, merge_request.author) - create_build_failed_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_build_succeeds? + create_build_failed_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_pipeline_succeeds? end # When a new commit is pushed to a merge request we should: @@ -121,7 +121,7 @@ class TodoService # def merge_request_build_retried(merge_request) mark_pending_todos_as_done(merge_request, merge_request.author) - mark_pending_todos_as_done(merge_request, merge_request.merge_user) if merge_request.merge_when_build_succeeds? + mark_pending_todos_as_done(merge_request, merge_request.merge_user) if merge_request.merge_when_pipeline_succeeds? end # When a merge request could not be automatically merged due to its unmergeable state we should: @@ -129,7 +129,7 @@ class TodoService # * create a todo for a merge_user # def merge_request_became_unmergeable(merge_request) - create_unmergeable_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_build_succeeds? + create_unmergeable_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_pipeline_succeeds? end # When create a note we should: diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb index fad741531ea..d9370bbb598 100644 --- a/app/services/users/refresh_authorized_projects_service.rb +++ b/app/services/users/refresh_authorized_projects_service.rb @@ -115,11 +115,23 @@ module Users # Returns a union query of projects that the user is authorized to access def project_authorizations_union relations = [ + # Personal projects user.personal_projects.select("#{user.id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"), - user.groups_projects.select_for_project_authorization, + + # Projects the user is a member of user.projects.select_for_project_authorization, + + # Projects of groups the user is a member of + user.groups_projects.select_for_project_authorization, + + # Projects of subgroups of groups the user is a member of + user.nested_groups_projects.select_for_project_authorization, + + # Projects shared with groups the user is a member of user.groups.joins(:shared_projects).select_for_project_authorization, - user.nested_projects.select_for_project_authorization + + # Projects shared with subgroups of groups the user is a member of + user.nested_groups.joins(:shared_projects).select_for_project_authorization ] Gitlab::SQL::Union.new(relations) diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml index 0dbb0ca6958..89d991abe54 100644 --- a/app/views/dashboard/_activities.html.haml +++ b/app/views/dashboard/_activities.html.haml @@ -2,10 +2,9 @@ = render "events/event_last_push", event: @last_push .nav-block - - if current_user - .controls - = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn has-tooltip', title: 'Subscribe' do - %i.fa.fa-rss + .controls + = link_to dashboard_projects_path(rss_url_options), class: 'btn rss-btn has-tooltip', title: 'Subscribe' do + %i.fa.fa-rss = render 'shared/event_filter' .content_list diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml index 23c145ebbb4..c6d5937a3c3 100644 --- a/app/views/dashboard/_groups_head.html.haml +++ b/app/views/dashboard/_groups_head.html.haml @@ -6,7 +6,10 @@ = nav_link(page: explore_groups_path) do = link_to explore_groups_path, title: 'Explore groups' do Explore Groups - - if current_user.can_create_group? - .nav-controls + .nav-controls + = form_tag request.path, method: :get, class: 'group-filter-form', id: 'group-filter-form' do |f| + = search_field_tag :filter_groups, params[:filter_groups], placeholder: 'Filter by name...', class: 'group-filter-form-field form-control input-short js-groups-list-filter', spellcheck: false, id: 'group-filter-form-field', tabindex: "2" + = render 'shared/groups/dropdown' + - if current_user.can_create_group? = link_to new_group_path, class: "btn btn-new" do New Group diff --git a/app/views/dashboard/activity.html.haml b/app/views/dashboard/activity.html.haml index aa57df14c23..190ad4b40a5 100644 --- a/app/views/dashboard/activity.html.haml +++ b/app/views/dashboard/activity.html.haml @@ -1,6 +1,5 @@ = content_for :meta_tags do - - if current_user - = auto_discovery_link_tag(:atom, dashboard_projects_url(format: :atom, private_token: current_user.private_token), title: "All activity") + = auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity") - page_title "Activity" - header_title "Activity", activity_dashboard_path diff --git a/app/views/dashboard/groups/_groups.html.haml b/app/views/dashboard/groups/_groups.html.haml new file mode 100644 index 00000000000..6c3bf1a2b3b --- /dev/null +++ b/app/views/dashboard/groups/_groups.html.haml @@ -0,0 +1,6 @@ +.js-groups-list-holder + %ul.content-list + - @group_members.each do |group_member| + = render 'shared/groups/group', group: group_member.group, group_member: group_member + + = paginate @group_members, theme: 'gitlab' diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index 1a679c51774..73ab2c95ff9 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -5,9 +5,4 @@ - if @group_members.empty? = render 'empty_state' - else - %ul.content-list - - @group_members.each do |group_member| - - group = group_member.group - = render 'shared/groups/group', group: group, group_member: group_member - - = paginate @group_members, theme: 'gitlab' + = render 'groups' diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml index 653052f7c54..9a4e423f896 100644 --- a/app/views/dashboard/issues.html.haml +++ b/app/views/dashboard/issues.html.haml @@ -1,17 +1,15 @@ - page_title "Issues" - header_title "Issues", issues_dashboard_path(assignee_id: current_user.id) = content_for :meta_tags do - - if current_user - = auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{current_user.name} issues") + = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{current_user.name} issues") .top-area = render 'shared/issuable/nav', type: :issues .nav-controls - - if current_user - = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do - = icon('rss') - %span.icon-label - Subscribe + = link_to params.merge(rss_url_options), class: 'btn' do + = icon('rss') + %span.icon-label + Subscribe = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue" = render 'shared/issuable/filter', type: :issues diff --git a/app/views/dashboard/projects/index.atom.builder b/app/views/dashboard/projects/index.atom.builder index fb5be63b472..13f7a8ddcec 100644 --- a/app/views/dashboard/projects/index.atom.builder +++ b/app/views/dashboard/projects/index.atom.builder @@ -1,7 +1,7 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do xml.title "Activity" - xml.link href: dashboard_projects_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" + xml.link href: dashboard_projects_url(rss_url_options), rel: "self", type: "application/atom+xml" xml.link href: dashboard_projects_url, rel: "alternate", type: "text/html" xml.id dashboard_projects_url xml.updated @events[0].updated_at.xmlschema if @events[0] diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml index b82b933c3ad..f0adbad8412 100644 --- a/app/views/dashboard/projects/index.html.haml +++ b/app/views/dashboard/projects/index.html.haml @@ -1,6 +1,5 @@ = content_for :meta_tags do - - if current_user - = auto_discovery_link_tag(:atom, dashboard_projects_url(format: :atom, private_token: current_user.private_token), title: "All activity") + = auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity") - page_title "Projects" - header_title "Projects", dashboard_projects_path diff --git a/app/views/explore/groups/_groups.html.haml b/app/views/explore/groups/_groups.html.haml new file mode 100644 index 00000000000..794c6d1d170 --- /dev/null +++ b/app/views/explore/groups/_groups.html.haml @@ -0,0 +1,6 @@ +.js-groups-list-holder + %ul.content-list + - @groups.each do |group| + = render 'shared/groups/group', group: group + + = paginate @groups, theme: 'gitlab' diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index 73cf6e87eb4..7f1bacc91cb 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -6,40 +6,10 @@ - else = render 'explore/head' -.row-content-block.clearfix - .pull-left - = form_tag explore_groups_path, method: :get, class: 'form-inline form-tiny' do |f| - = hidden_field_tag :sort, @sort - .form-group - = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input", id: "groups_search", spellcheck: false - .form-group - = button_tag 'Search', class: "btn btn-default" - - .pull-right - .dropdown.inline - %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' } - %span.light - - if @sort.present? - = sort_options_hash[@sort] - - else - = sort_title_recently_created - = icon('chevron-down') - %ul.dropdown-menu.dropdown-menu-align-right - %li - = link_to explore_groups_path(sort: sort_value_recently_created) do - = sort_title_recently_created - = link_to explore_groups_path(sort: sort_value_oldest_created) do - = sort_title_oldest_created - = link_to explore_groups_path(sort: sort_value_recently_updated) do - = sort_title_recently_updated - = link_to explore_groups_path(sort: sort_value_oldest_updated) do - = sort_title_oldest_updated - -%ul.content-list - - @groups.each do |group| - = render 'shared/groups/group', group: group - - unless @groups.present? - .nothing-here-block No public groups +- if @groups.present? + = render 'groups' +- else + .nothing-here-block No public groups = paginate @groups, theme: "gitlab" diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml index c442cf056c3..d7851c79990 100644 --- a/app/views/groups/_activities.html.haml +++ b/app/views/groups/_activities.html.haml @@ -2,10 +2,9 @@ = render "events/event_last_push", event: @last_push .nav-block - - if current_user - .controls - = link_to group_path(@group, format: :atom, private_token: current_user.private_token), class: 'btn rss-btn has-tooltip' , title: 'Subscribe' do - %i.fa.fa-rss + .controls + = link_to group_path(@group, rss_url_options), class: 'btn rss-btn has-tooltip' , title: 'Subscribe' do + %i.fa.fa-rss = render 'shared/event_filter' .content_list diff --git a/app/views/groups/_head.html.haml b/app/views/groups/_head.html.haml index 6b296ea8dea..873504099d4 100644 --- a/app/views/groups/_head.html.haml +++ b/app/views/groups/_head.html.haml @@ -3,7 +3,7 @@ = render 'shared/nav_scroll' .nav-links.sub-nav.scrolling-tabs %ul{ class: container_class } - = nav_link(path: 'groups#show', html_options: { class: 'home' }) do + = nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do = link_to group_path(@group), title: 'Group Home' do %span Home @@ -12,8 +12,3 @@ = link_to activity_group_path(@group), title: 'Activity' do %span Activity - - = nav_link(path: 'group_members#index') do - = link_to group_group_members_path(@group), title: 'Members' do - %span - Members diff --git a/app/views/groups/activity.html.haml b/app/views/groups/activity.html.haml index d7375b23524..3969e56f937 100644 --- a/app/views/groups/activity.html.haml +++ b/app/views/groups/activity.html.haml @@ -1,6 +1,5 @@ = content_for :meta_tags do - - if current_user - = auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity") + = auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity") - page_title "Activity" = render 'groups/head' diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index 8cb56443191..2e4e4511bb6 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -1,5 +1,4 @@ - page_title "Members" -= render 'groups/head' .project-members-page.prepend-top-default %h4 diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index 939bddf3fe9..f4c17dc2d16 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -1,19 +1,17 @@ - page_title "Issues" = render "head_issues" = content_for :meta_tags do - - if current_user - = auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@group.name} issues") + = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@group.name} issues") - if group_issues(@group).exists? .top-area = render 'shared/issuable/nav', type: :issues - - if current_user - .nav-controls - = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do - = icon('rss') - %span.icon-label - Subscribe - = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue" + .nav-controls + = link_to params.merge(rss_url_options), class: 'btn' do + = icon('rss') + %span.icon-label + Subscribe + = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue" = render 'shared/issuable/filter', type: :issues diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder index b68bf444d27..914091dfd15 100644 --- a/app/views/groups/show.atom.builder +++ b/app/views/groups/show.atom.builder @@ -1,7 +1,7 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do xml.title "#{@group.name} activity" - xml.link href: group_url(@group, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" + xml.link href: group_url(@group, rss_url_options), rel: "self", type: "application/atom+xml" xml.link href: group_url(@group), rel: "alternate", type: "text/html" xml.id group_url(@group) xml.updated @events[0].updated_at.xmlschema if @events[0] diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 3d7b469660a..8f0f2708194 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -1,8 +1,7 @@ - @no_container = true = content_for :meta_tags do - - if current_user - = auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity") + = auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity") = render 'groups/head' = render 'groups/home_panel' diff --git a/app/views/groups/subgroups.html.haml b/app/views/groups/subgroups.html.haml index 8610ae7e0ef..be809083139 100644 --- a/app/views/groups/subgroups.html.haml +++ b/app/views/groups/subgroups.html.haml @@ -1,5 +1,6 @@ - @no_container = true += render 'head' = render 'groups/home_panel' .groups-header{ class: container_class } diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 705e20112fa..5d1369c2010 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -131,12 +131,6 @@ %tr %td.shortcut .key g - .key e - %td - Go to the project's activity feed - %tr - %td.shortcut - .key g .key f %td Go to files @@ -161,12 +155,6 @@ %tr %td.shortcut .key g - .key g - %td - Go to graphs - %tr - %td.shortcut - .key g .key i %td Go to issues diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 0b8388cbff3..555ec8ad079 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -36,6 +36,10 @@ = icon('bell fw') %span.badge.todos-pending-count{ class: ("hidden" if todos_pending_count == 0) } = todos_count_format(todos_pending_count) + - if current_user.can_create_project? + %li + = link_to new_project_path, title: 'New project', aria: { label: "New project" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = icon('plus fw') - if Gitlab::Sherlock.enabled? %li = link_to sherlock_transactions_path, title: 'Sherlock Transactions', @@ -51,8 +55,6 @@ = link_to "Profile", current_user, class: 'profile-link', aria: { label: "Profile" }, data: { user: current_user.username } %li = link_to "Settings", profile_path, aria: { label: "Settings" } - %li - = link_to "Help", help_path, aria: { label: "Help" } %li.divider %li = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link", aria: { label: "Sign out" } @@ -61,12 +63,12 @@ %div = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success' - %h1.title= title - .header-logo = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do = brand_header_logo + %h1.title= title + = yield :header_content = render 'shared/outdated_browser' diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 5d4178f03d7..15285ee32a3 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -24,16 +24,16 @@ = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do %span Issues - (#{number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened))}) + .badge= number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened)) = nav_link(path: 'dashboard#merge_requests') do = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do %span Merge Requests - (#{number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened))}) + .badge= number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened)) = nav_link(controller: 'dashboard/snippets') do = link_to dashboard_snippets_path, title: 'Snippets' do %span Snippets %li.divider %li - = link_to "About GitLab CE", help_path, title: 'About GitLab CE', class: 'about-gitlab' + = link_to "Help", help_path, title: 'About GitLab CE', class: 'about-gitlab' diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index e0742d70fac..a6e96942021 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -5,7 +5,7 @@ .fade-right = icon('angle-right') %ul.nav-links.scrolling-tabs - = nav_link(path: ['groups#show', 'groups#activity', 'group_members#index'], html_options: { class: 'home' }) do + = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do = link_to group_path(@group), title: 'Home' do %span Group @@ -21,3 +21,7 @@ Merge Requests - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute %span.badge.count= number_with_delimiter(merge_requests.count) + = nav_link(path: 'group_members#index') do + = link_to group_group_members_path(@group), title: 'Members' do + %span + Members diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 7883823b21e..2335d467389 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -21,40 +21,23 @@ .fade-right = icon('angle-right') %ul.nav-links.scrolling-tabs - = nav_link(path: 'projects#show', html_options: {class: 'home'}) do + = nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do %span Project - = nav_link(path: 'projects#activity') do - = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do - %span - Activity - - if project_nav_tab? :files - = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare repositories tags branches releases network)) do + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare repositories tags branches releases graphs network)) do = link_to project_files_path(@project), title: 'Repository', class: 'shortcuts-tree' do %span Repository - - if project_nav_tab? :pipelines - = nav_link(controller: [:pipelines, :builds, :environments, :cycle_analytics]) do - = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do - %span - Pipelines - - if project_nav_tab? :container_registry = nav_link(controller: %w(container_registry)) do = link_to project_container_registry_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do %span Registry - - if project_nav_tab? :graphs - = nav_link(controller: %w(graphs)) do - = link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs' do - %span - Graphs - - if project_nav_tab? :issues = nav_link(controller: [:issues, :labels, :milestones, :boards]) do = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues', class: 'shortcuts-issues' do @@ -70,6 +53,12 @@ Merge Requests %span.badge.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) + - if project_nav_tab? :pipelines + = nav_link(controller: [:pipelines, :builds, :environments]) do + = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do + %span + Pipelines + - if project_nav_tab? :wiki = nav_link(controller: :wikis) do = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml index 4268337fd6d..fb990dd9592 100644 --- a/app/views/projects/_activity.html.haml +++ b/app/views/projects/_activity.html.haml @@ -1,11 +1,11 @@ - @no_container = true += render "projects/head" %div{ class: container_class } .nav-block.activity-filter-block - - if current_user - .controls - = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Subscribe", class: 'btn rss-btn has-tooltip' do - = icon('rss') + .controls + = link_to namespace_project_path(@project.namespace, @project, rss_url_options), title: "Subscribe", class: 'btn rss-btn has-tooltip' do + = icon('rss') = render 'shared/event_filter' diff --git a/app/views/projects/_head.html.haml b/app/views/projects/_head.html.haml new file mode 100644 index 00000000000..db08b77c8e0 --- /dev/null +++ b/app/views/projects/_head.html.haml @@ -0,0 +1,20 @@ += content_for :sub_nav do + .scrolling-tabs-container.sub-nav-scroll + = render 'shared/nav_scroll' + .nav-links.sub-nav.scrolling-tabs + %ul{ class: container_class } + = nav_link(path: 'projects#show') do + = link_to project_path(@project), title: 'Project home', class: 'shortcuts-project' do + %span + Home + + = nav_link(path: 'projects#activity') do + = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do + %span + Activity + + - if can?(current_user, :read_cycle_analytics, @project) + = nav_link(path: 'cycle_analytics#show') do + = link_to project_cycle_analytics_path(@project), title: 'Cycle Analytics', class: 'shortcuts-project-cycle-analytics' do + %span + Cycle Analytics diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml index 27d25a6b682..188198c47d5 100644 --- a/app/views/projects/_merge_request_merge_settings.html.haml +++ b/app/views/projects/_merge_request_merge_settings.html.haml @@ -2,8 +2,8 @@ .form-group .checkbox.builds-feature - = form.label :only_allow_merge_if_build_succeeds do - = form.check_box :only_allow_merge_if_build_succeeds + = form.label :only_allow_merge_if_pipeline_succeeds do + = form.check_box :only_allow_merge_if_pipeline_succeeds %strong Only allow merge requests to be merged if the pipeline succeeds %br %span.descr diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml index 7b9cfbbd067..c44d8fcd430 100644 --- a/app/views/projects/blob/_actions.html.haml +++ b/app/views/projects/blob/_actions.html.haml @@ -1,7 +1,8 @@ -.btn-group - = view_on_environment_button(@commit.sha, @path, @environment) if @environment +- if @environment + .btn-group< + = view_on_environment_button(@commit.sha, @path, @environment) -.btn-group.tree-btn-group +.btn-group{ role: "group" }< = link_to 'Raw', namespace_project_raw_path(@project.namespace, @project, @id), class: 'btn btn-sm', target: '_blank' -# only show normal/blame view links for text files @@ -18,7 +19,7 @@ tree_join(@commit.sha, @path)), class: 'btn btn-sm js-data-file-blob-permalink-url' - if current_user - .btn-group{ role: "group" } + .btn-group{ role: "group" }< - if blob_text_viewable?(@blob) = edit_blob_link = replace_blob_link diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index 19fa4c78501..41a7191302d 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -24,12 +24,13 @@ #blob-content-holder.blob-content-holder %article.file-holder - .js-file-title.file-title - = blob_icon blob.mode, blob.name - %strong - = blob.name - %small - = number_to_human_size(blob_size(blob)) + .js-file-title.file-title-flex-parent + .file-header-content + = blob_icon blob.mode, blob.name + %strong.file-title-name + = blob.name + %small + = number_to_human_size(blob_size(blob)) .file-actions.hidden-xs = render "actions" = render blob, blob: blob diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml index c48d9dd982c..d1d448f0d4c 100644 --- a/app/views/projects/blob/diff.html.haml +++ b/app/views/projects/blob/diff.html.haml @@ -9,20 +9,20 @@ - line_old = line_new - @form.offset - line_content = capture do %td.line_content.noteable_line{ class: line_class }==#{' ' * @form.indent}#{line} - %tr.line_holder{ id: line_old, class: line_class } + %tr.line_holder.diff-expanded{ id: line_old, class: line_class } - case diff_view - when :inline %td.old_line.diff-line-num{ data: { linenumber: line_old } } - %a{ href: "##{line_old}", data: { linenumber: line_old } } + %a{ href: "#", data: { linenumber: line_old }, disabled: true } %td.new_line.diff-line-num{ data: { linenumber: line_new } } - %a{ href: "##{line_new}", data: { linenumber: line_new } } + %a{ href: "#", data: { linenumber: line_new }, disabled: true } = line_content - when :parallel %td.old_line.diff-line-num{ data: { linenumber: line_old } } - %a{ href: "##{line_old}", data: { linenumber: line_old } } + %a{ href: "##{line_old}", data: { linenumber: line_old }, disabled: true } = line_content %td.new_line.diff-line-num{ data: { linenumber: line_new } } - %a{ href: "##{line_new}", data: { linenumber: line_new } } + %a{ href: "##{line_new}", data: { linenumber: line_new }, disabled: true } = line_content - if @form.unfold? && @form.bottom? && @form.to < @blob.lines.size diff --git a/app/views/projects/boards/components/_board_list.html.haml b/app/views/projects/boards/components/_board_list.html.haml index f413a5e94c1..0993e880da9 100644 --- a/app/views/projects/boards/components/_board_list.html.haml +++ b/app/views/projects/boards/components/_board_list.html.haml @@ -2,28 +2,8 @@ .board-list-loading.text-center{ "v-if" => "loading" } = icon("spinner spin") - if can? current_user, :create_issue, @project - %board-new-issue{ "inline-template" => true, - ":list" => "list", + %board-new-issue{ ":list" => "list", "v-if" => 'list.type !== "done" && showIssueForm' } - .card.board-new-issue-form - %form{ "@submit" => "submit($event)" } - .flash-container{ "v-if" => "error" } - .flash-alert - An error occured. Please try again. - %label.label-light{ ":for" => 'list.id + "-title"' } - Title - %input.form-control{ type: "text", - "v-model" => "title", - "ref" => "input", - ":id" => 'list.id + "-title"' } - .clearfix.prepend-top-10 - %button.btn.btn-success.pull-left{ type: "submit", - ":disabled" => 'title === ""', - "ref" => "submit-button" } - Submit issue - %button.btn.btn-default.pull-right{ type: "button", - "@click" => "cancel" } - Cancel %ul.board-list{ "ref" => "list", "v-show" => "!loading", ":data-board" => "list.id", diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml index 27e81c2bec3..7eb17e887e7 100644 --- a/app/views/projects/builds/_header.html.haml +++ b/app/views/projects/builds/_header.html.haml @@ -1,4 +1,4 @@ -.content-block.build-header +.content-block.build-header.top-area .header-content = render 'ci/status/badge', status: @build.detailed_status(current_user), link: false Job @@ -16,7 +16,10 @@ - if @build.user = render "user" = time_ago_with_tooltip(@build.created_at) - - if can?(current_user, :update_build, @build) && @build.retryable? - = link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary pull-right', method: :post + .nav-controls + - if can?(current_user, :create_issue, @project) && @build.failed? + = link_to "New issue", new_namespace_project_issue_path(@project.namespace, @project, issue: build_failed_issue_options), class: 'btn btn-new btn-inverted' + - if can?(current_user, :update_build, @build) && @build.retryable? + = link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary', method: :post %button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" } = icon('angle-double-left') diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index 421b3db342d..2ebd4f9069a 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -1,10 +1,10 @@ - case type.to_s - when 'revert' - label = 'Revert' - - target_label = 'Revert in branch' + - branch_label = 'Revert in branch' - when 'cherry-pick' - label = 'Cherry-pick' - - target_label = 'Pick into branch' + - branch_label = 'Pick into branch' .modal{ id: "modal-#{type}-commit" } .modal-dialog @@ -15,10 +15,10 @@ .modal-body = form_tag [type.underscore, @project.namespace.becomes(Namespace), @project, commit], method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do .form-group.branch - = label_tag 'target_branch', target_label, class: 'control-label' + = label_tag 'start_branch', branch_label, class: 'control-label' .col-sm-10 - = hidden_field_tag :target_branch, @project.default_branch, id: 'target_branch' - = dropdown_tag(@project.default_branch, options: { title: "Switch branch", filter: true, placeholder: "Search branches", toggle_class: 'js-project-refs-dropdown js-target-branch dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "target_branch", selected: @project.default_branch, target_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false } }) + = hidden_field_tag :start_branch, @project.default_branch, id: 'start_branch' + = dropdown_tag(@project.default_branch, options: { title: "Switch branch", filter: true, placeholder: "Search branches", toggle_class: 'js-project-refs-dropdown js-target-branch dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false } }) - if can?(current_user, :push_code, @project) .js-create-merge-request-container diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 33917513f37..6792b3f7a83 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -2,27 +2,6 @@ #commit-pipeline-table-view{ data: { disable_initialization: disable_initialization, endpoint: endpoint, } } -.pipeline-svgs{ data: { "commit_icon_svg" => custom_icon("icon_commit"), - "icon_status_canceled" => custom_icon("icon_status_canceled"), - "icon_status_running" => custom_icon("icon_status_running"), - "icon_status_skipped" => custom_icon("icon_status_skipped"), - "icon_status_created" => custom_icon("icon_status_created"), - "icon_status_pending" => custom_icon("icon_status_pending"), - "icon_status_success" => custom_icon("icon_status_success"), - "icon_status_failed" => custom_icon("icon_status_failed"), - "icon_status_warning" => custom_icon("icon_status_warning"), - "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"), - "stage_icon_status_running" => custom_icon("icon_status_running_borderless"), - "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"), - "stage_icon_status_created" => custom_icon("icon_status_created_borderless"), - "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"), - "stage_icon_status_success" => custom_icon("icon_status_success_borderless"), - "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"), - "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"), - "icon_play" => custom_icon("icon_play"), - "icon_timer" => custom_icon("icon_timer"), - "icon_status_manual" => custom_icon("icon_status_manual"), -} } - content_for :page_specific_javascripts do = page_specific_javascript_bundle_tag('commit_pipelines') diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index 80763ce67ca..dd6797f10c0 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -11,14 +11,6 @@ = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do Commits - = nav_link(controller: %w(network)) do - = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do - Network - - = nav_link(controller: :compare) do - = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do - Compare - = nav_link(html_options: {class: branches_tab_class}) do = link_to namespace_project_branches_path(@project.namespace, @project) do Branches @@ -26,3 +18,19 @@ = nav_link(controller: [:tags, :releases]) do = link_to namespace_project_tags_path(@project.namespace, @project) do Tags + + = nav_link(path: 'graphs#show') do + = link_to namespace_project_graph_path(@project.namespace, @project, current_ref) do + Contributors + + = nav_link(controller: %w(network)) do + = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do + Graph + + = nav_link(controller: :compare) do + = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do + Compare + + = nav_link(path: 'graphs#charts') do + = link_to charts_namespace_project_graph_path(@project.namespace, @project, current_ref) do + Charts diff --git a/app/views/projects/commits/show.atom.builder b/app/views/projects/commits/show.atom.builder index 30bb7412073..2f0b6e39800 100644 --- a/app/views/projects/commits/show.atom.builder +++ b/app/views/projects/commits/show.atom.builder @@ -1,7 +1,7 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do xml.title "#{@project.name}:#{@ref} commits" - xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" + xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref, rss_url_options), rel: "self", type: "application/atom+xml" xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref), rel: "alternate", type: "text/html" xml.id namespace_project_commits_url(@project.namespace, @project, @ref) xml.updated @commits.first.committed_date.xmlschema if @commits.any? diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index 08cb8a04413..38dbf2ac10b 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -2,8 +2,7 @@ - page_title "Commits", @ref = content_for :meta_tags do - - if current_user - = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits") + = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits") = content_for :sub_nav do = render "head" @@ -27,10 +26,9 @@ .control = form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'commits-search-form') do = search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false } - - if current_user && current_user.private_token - .control - = link_to namespace_project_commits_path(@project.namespace, @project, @ref, { format: :atom, private_token: current_user.private_token }), title: "Commits Feed", class: 'btn' do - = icon("rss") + .control + = link_to namespace_project_commits_path(@project.namespace, @project, @ref, rss_url_options), title: "Commits Feed", class: 'btn' do + = icon("rss") %div{ id: dom_id(@project) } %ol#commits-list.list-unstyled.content_list diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 5405ff16bea..be17f2c764e 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -3,7 +3,7 @@ - content_for :page_specific_javascripts do = page_specific_javascript_bundle_tag('cycle_analytics') -= render "projects/pipelines/head" += render "projects/head" #cycle-analytics{ class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) } } - if @cycle_analytics_no_data diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 1f27d41ddd9..d6366b57957 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -13,7 +13,4 @@ "project-stopped-environments-path" => project_environments_path(@project, scope: :stopped), "new-environment-path" => new_namespace_project_environment_path(@project.namespace, @project), "help-page-path" => help_page_path("ci/environments"), - "css-class" => container_class, - "commit-icon-svg" => custom_icon("icon_commit"), - "terminal-icon-svg" => custom_icon("icon_terminal"), - "play-icon-svg" => custom_icon("icon_play") } } + "css-class" => container_class } } diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml deleted file mode 100644 index 67018aaa2ac..00000000000 --- a/app/views/projects/graphs/_head.html.haml +++ /dev/null @@ -1,19 +0,0 @@ -= content_for :sub_nav do - .scrolling-tabs-container.sub-nav-scroll - = render 'shared/nav_scroll' - .nav-links.sub-nav.scrolling-tabs - %ul{ class: (container_class) } - - - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('lib_chart') - = page_specific_javascript_bundle_tag('graphs') - = nav_link(action: :show) do - = link_to 'Contributors', namespace_project_graph_path - = nav_link(action: :commits) do - = link_to 'Commits', commits_namespace_project_graph_path - = nav_link(action: :languages) do - = link_to 'Languages', languages_namespace_project_graph_path - - if @project.feature_available?(:builds, current_user) - = nav_link(action: :ci) do - = link_to ci_namespace_project_graph_path do - Continuous Integration diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/charts.html.haml index c8a82f7bca3..d3bce45e974 100644 --- a/app/views/projects/graphs/commits.html.haml +++ b/app/views/projects/graphs/charts.html.haml @@ -1,38 +1,58 @@ - @no_container = true -- page_title "Commits", "Graphs" -= render 'head' +- page_title "Charts" +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('lib_chart') + = page_specific_javascript_bundle_tag('graphs') += render "projects/commits/head" -%div{ class: container_class } - .sub-header-block - .tree-ref-holder - = render 'shared/ref_switcher', destination: 'graphs_commits' - %ul.breadcrumb.repo-breadcrumb - = commits_breadcrumbs +.repo-charts{ class: container_class } + %h4.sub-header + Programming languages used in this repository - %p.lead - Commit statistics for - %strong= @ref - #{@commits_graph.start_date.strftime('%b %d')} - #{@commits_graph.end_date.strftime('%b %d')} + .row + .col-md-4 + %ul.bordered-list + - @languages.each do |language| + %li + %span{ style: "color: #{language[:color]}" } + = icon('circle') + + = language[:label] + .pull-right + = language[:value] + \% + .col-md-8 + %canvas#languages-chart{ height: 400 } + +.repo-charts{ class: container_class } + .sub-header-block.border-top + + .row.tree-ref-header + .col-md-6 + %h4 + Commit statistics for + %strong= @ref + #{@commits_graph.start_date.strftime('%b %d')} - #{@commits_graph.end_date.strftime('%b %d')} + + .col-md-6 + .tree-ref-container + .tree-ref-holder + = render 'shared/ref_switcher', destination: 'graphs_commits' + %ul.breadcrumb.repo-breadcrumb + = commits_breadcrumbs .row .col-md-6 - %ul + %ul.commit-stats %li - %p.lead - %strong= @commits_graph.commits.size - commits during - %strong= @commits_graph.duration - days + Total: + %strong #{@commits_graph.commits.size} commits %li - %p.lead - Average - %strong= @commits_graph.commit_per_day - commits per day + Average per day: + %strong #{@commits_graph.commit_per_day} commits %li - %p.lead - Contributed by - %strong= @commits_graph.authors - authors + Authors: + %strong= @commits_graph.authors .col-md-6 %div %p.slead @@ -40,15 +60,18 @@ %canvas#month-chart .row .col-md-6 - %div - %p.slead - Commits per day hour (UTC) - %canvas#hour-chart .col-md-6 %div %p.slead Commits per weekday %canvas#weekday-chart + .row + .col-md-6 + .col-md-6 + %div + %p.slead + Commits per day hour (UTC) + %canvas#hour-chart :javascript var responsiveChart = function (selector, data) { @@ -93,3 +116,12 @@ var monthData = chartData(#{@commits_per_month.keys.to_json}, #{@commits_per_month.values.to_json}); responsiveChart($('#month-chart'), monthData); + + var data = #{@languages.to_json}; + var ctx = $("#languages-chart").get(0).getContext("2d"); + var options = { + scaleOverlay: true, + responsive: true, + maintainAspectRatio: false + } + var myPieChart = new Chart(ctx).Pie(data, options); diff --git a/app/views/projects/graphs/ci.html.haml b/app/views/projects/graphs/ci.html.haml deleted file mode 100644 index 6be4273b6ab..00000000000 --- a/app/views/projects/graphs/ci.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -- @no_container = true -- page_title "Continuous Integration", "Graphs" -= render 'head' - -%div{ class: container_class } - .sub-header-block - .oneline - A collection of graphs for Continuous Integration - - #charts.ci-charts - .row - .col-md-6 - = render 'projects/graphs/ci/overall' - .col-md-6 - = render 'projects/graphs/ci/build_times' - - %hr - = render 'projects/graphs/ci/builds' diff --git a/app/views/projects/graphs/languages.html.haml b/app/views/projects/graphs/languages.html.haml deleted file mode 100644 index fcfcae0be20..00000000000 --- a/app/views/projects/graphs/languages.html.haml +++ /dev/null @@ -1,33 +0,0 @@ -- @no_container = true -- page_title "Languages", "Graphs" -= render 'head' - -%div{ class: container_class } - .sub-header-block - .oneline - Programming languages used in this repository - - .row - .col-md-8 - %canvas#languages-chart{ height: 400 } - .col-md-4 - %ul.bordered-list - - @languages.each do |language| - %li - %span{ style: "color: #{language[:color]}" } - = icon('circle') - - = language[:label] - .pull-right - = language[:value] - \% - -:javascript - var data = #{@languages.to_json}; - var ctx = $("#languages-chart").get(0).getContext("2d"); - var options = { - scaleOverlay: true, - responsive: true, - maintainAspectRatio: false - } - var myPieChart = new Chart(ctx).Pie(data, options); diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index 5ebb939a109..d89dfe31e47 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -1,6 +1,9 @@ - @no_container = true -- page_title "Contributors", "Graphs" -= render 'head' +- page_title "Contributors" +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('lib_chart') + = page_specific_javascript_bundle_tag('graphs') += render 'projects/commits/head' %div{ class: container_class } .sub-header-block diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml index 4825820c4d9..7a188cb6445 100644 --- a/app/views/projects/issues/_head.html.haml +++ b/app/views/projects/issues/_head.html.haml @@ -7,7 +7,7 @@ = nav_link(controller: :issues) do = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues' do %span - Issues + List = nav_link(controller: :boards) do = link_to namespace_project_boards_path(@project.namespace, @project), title: 'Board' do diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 8ea1a3a45e1..7b7d7b1e00e 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -10,17 +10,15 @@ = page_specific_javascript_bundle_tag('filtered_search') = content_for :meta_tags do - - if current_user - = auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@project.name} issues") + = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues") - if project_issues(@project).exists? %div{ class: (container_class) } .top-area = render 'shared/issuable/nav', type: :issues .nav-controls - - if current_user - = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn append-right-10 has-tooltip', title: 'Subscribe' do - = icon('rss') + = link_to params.merge(rss_url_options), class: 'btn append-right-10 has-tooltip', title: 'Subscribe' do + = icon('rss') - if can? current_user, :create_issue, @project = link_to new_namespace_project_issue_path(@project.namespace, @project, diff --git a/app/views/projects/merge_requests/cancel_merge_when_build_succeeds.js.haml b/app/views/projects/merge_requests/cancel_merge_when_pipeline_succeeds.js.haml index eab5be488b5..eab5be488b5 100644 --- a/app/views/projects/merge_requests/cancel_merge_when_build_succeeds.js.haml +++ b/app/views/projects/merge_requests/cancel_merge_when_pipeline_succeeds.js.haml diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index 83e6c026ba7..8a96c8dacf6 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -2,7 +2,6 @@ - @bulk_edit = can?(current_user, :admin_merge_request, @project) - page_title "Merge Requests" -= render "projects/issues/head" = render 'projects/last_push' - content_for :page_specific_javascripts do diff --git a/app/views/projects/merge_requests/merge.js.haml b/app/views/projects/merge_requests/merge.js.haml index 84b6c9ebc5c..f0a23bec5e7 100644 --- a/app/views/projects/merge_requests/merge.js.haml +++ b/app/views/projects/merge_requests/merge.js.haml @@ -2,9 +2,9 @@ - when :success :plain merge_request_widget.mergeInProgress(#{params[:should_remove_source_branch] == '1'}); -- when :merge_when_build_succeeds +- when :merge_when_pipeline_succeeds :plain - $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/merge_when_build_succeeds'))}"); + $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/merge_when_pipeline_succeeds'))}"); - when :sha_mismatch :plain $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/sha_mismatch'))}"); diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index c0d6ab669b8..f0ccc4e00fd 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -19,8 +19,8 @@ = render 'projects/merge_requests/widget/open/conflicts' - elsif @merge_request.work_in_progress? = render 'projects/merge_requests/widget/open/wip' - - elsif @merge_request.merge_when_build_succeeds? - = render 'projects/merge_requests/widget/open/merge_when_build_succeeds' + - elsif @merge_request.merge_when_pipeline_succeeds? + = render 'projects/merge_requests/widget/open/merge_when_pipeline_succeeds' - elsif !@merge_request.can_be_merged_by?(current_user) = render 'projects/merge_requests/widget/open/not_allowed' - elsif !@merge_request.mergeable_ci_state? && (@pipeline.failed? || @pipeline.canceled?) diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index b730ced4214..c94c7944c0b 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -1,8 +1,6 @@ - content_for :page_specific_javascripts do = page_specific_javascript_bundle_tag('merge_request_widget') -- status_class = @pipeline ? " ci-#{@pipeline.status}" : nil - = form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-quick-submit js-requires-input' } do |f| = hidden_field_tag :authenticity_token, form_authenticity_token = hidden_field_tag :sha, @merge_request.diff_head_sha @@ -11,24 +9,24 @@ .accept-action - if @pipeline && @pipeline.active? %span.btn-group - = button_tag class: "btn btn-create js-merge-button merge_when_build_succeeds" do + = button_tag class: "btn btn-info js-merge-when-pipeline-succeeds-button merge-when-pipeline-succeeds" do Merge When Pipeline Succeeds - - unless @project.only_allow_merge_if_build_succeeds? - = button_tag class: "btn btn-success dropdown-toggle", 'data-toggle' => 'dropdown' do + - unless @project.only_allow_merge_if_pipeline_succeeds? + = button_tag class: "btn btn-info dropdown-toggle", 'data-toggle' => 'dropdown' do = icon('caret-down') %span.sr-only Select Merge Moment %ul.js-merge-dropdown.dropdown-menu.dropdown-menu-right{ role: 'menu' } %li - = link_to "#", class: "merge_when_build_succeeds" do + = link_to "#", class: "merge_when_pipeline_succeeds" do = icon('check fw') Merge When Pipeline Succeeds %li - = link_to "#", class: "accept_merge_request" do + = link_to "#", class: "accept-merge-request" do = icon('warning fw') Merge Immediately - else - = f.button class: "btn btn-create btn-grouped js-merge-button accept_merge_request #{status_class}" do + = f.button class: "btn btn-grouped js-merge-button accept-merge-request" do Accept Merge Request - if @merge_request.force_remove_source_branch? .accept-control @@ -49,4 +47,4 @@ text: @merge_request.merge_commit_message, rows: 14, hint: true - = hidden_field_tag :merge_when_build_succeeds, "", autocomplete: "off" + = hidden_field_tag :merge_when_pipeline_succeeds, "", autocomplete: "off" diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_pipeline_succeeds.html.haml index cf7abf3756c..40a683d3fbd 100644 --- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml +++ b/app/views/projects/merge_requests/widget/open/_merge_when_pipeline_succeeds.html.haml @@ -15,7 +15,7 @@ The source branch will not be removed. - remove_source_branch_button = !@merge_request.remove_source_branch? && @merge_request.can_remove_source_branch?(current_user) && @merge_request.merge_user == current_user - - user_can_cancel_automatic_merge = @merge_request.can_cancel_merge_when_build_succeeds?(current_user) + - user_can_cancel_automatic_merge = @merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user) - if remove_source_branch_button || user_can_cancel_automatic_merge .clearfix.prepend-top-10 - if remove_source_branch_button @@ -24,5 +24,5 @@ Remove Source Branch When Merged - if user_can_cancel_automatic_merge - = link_to cancel_merge_when_build_succeeds_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request), remote: true, method: :post, class: "btn btn-grouped btn-sm" do + = link_to cancel_merge_when_pipeline_succeeds_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request), remote: true, method: :post, class: "btn btn-grouped btn-sm" do Cancel Automatic Merge diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 06a31698ee6..b4dde2c86c9 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -19,10 +19,9 @@ Open .header-text-content %span.identifier - Milestone ##{@milestone.iid} + %strong + Milestone %#{@milestone.iid} - if @milestone.due_date || @milestone.start_date - %span.creator - · = milestone_date_range(@milestone) .milestone-buttons - if can?(current_user, :admin_milestone, @project) @@ -47,7 +46,7 @@ = preserve do = markdown_field(@milestone, :description) - - if @milestone.total_items_count(current_user).zero? + - if can?(current_user, :read_issue, @project) && @milestone.total_items_count(current_user).zero? .alert.alert-success.prepend-top-default %span Assign some issues to this milestone. - elsif @milestone.complete?(current_user) && @milestone.active? diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index b88eef65cef..a4a24a217d3 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -1,4 +1,4 @@ -- page_title "Network", @ref +- page_title "Graph", @ref - content_for :page_specific_javascripts do = page_specific_javascript_tag('lib/raphael.js') = page_specific_javascript_bundle_tag('network') diff --git a/app/views/projects/pages/_use.html.haml b/app/views/projects/pages/_use.html.haml index 9db46f0b1fc..e442e6e9a09 100644 --- a/app/views/projects/pages/_use.html.haml +++ b/app/views/projects/pages/_use.html.haml @@ -5,4 +5,6 @@ .panel-body %p Learn how to upload your static site and have it served by - GitLab by following the #{link_to "documentation on GitLab Pages", "http://doc.gitlab.com/ee/pages/README.html", target: :blank}. + GitLab by following the + = succeed '.' do + = link_to 'documentation on GitLab Pages', help_page_path('user/project/pages/index.md'), target: '_blank' diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml index 721a9b6beb5..a5acb7ac4a5 100644 --- a/app/views/projects/pipelines/_head.html.haml +++ b/app/views/projects/pipelines/_head.html.haml @@ -4,25 +4,25 @@ .nav-links.sub-nav.scrolling-tabs{ class: ('build' if local_assigns.fetch(:build_subnav, false)) } %ul{ class: (container_class) } - if project_nav_tab? :pipelines - = nav_link(controller: :pipelines) do + = nav_link(path: 'pipelines#index', controller: :pipelines) do = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do %span Pipelines - if project_nav_tab? :builds - = nav_link(controller: %w(builds)) do + = nav_link(path: 'builds#index', controller: :builds) do = link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do %span Jobs - if project_nav_tab? :environments - = nav_link(controller: %w(environments)) do + = nav_link(path: 'environments#index', controller: :environments) do = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do %span Environments - - if can?(current_user, :read_cycle_analytics, @project) - = nav_link(controller: %w(cycle_analytics)) do - = link_to project_cycle_analytics_path(@project), title: 'Cycle Analytics' do + - if @project.feature_available?(:builds, current_user) && !@project.empty_repo? + = nav_link(path: 'pipelines#charts') do + = link_to charts_namespace_project_pipelines_path(@project.namespace, @project), title: 'Charts', class: 'shortcuts-pipelines-charts' do %span - Cycle Analytics + Charts diff --git a/app/views/projects/pipelines/charts.html.haml b/app/views/projects/pipelines/charts.html.haml new file mode 100644 index 00000000000..8ffdfa1a2cf --- /dev/null +++ b/app/views/projects/pipelines/charts.html.haml @@ -0,0 +1,21 @@ +- @no_container = true +- page_title "Charts", "Pipelines" +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('lib_chart') + = page_specific_javascript_bundle_tag('graphs') += render 'head' + +%div{ class: container_class } + .sub-header-block + .oneline + A collection of graphs for Continuous Integration + + #charts.ci-charts + .row + .col-md-6 + = render 'projects/pipelines/charts/overall' + .col-md-6 + = render 'projects/pipelines/charts/build_times' + + %hr + = render 'projects/pipelines/charts/builds' diff --git a/app/views/projects/graphs/ci/_build_times.haml b/app/views/projects/pipelines/charts/_build_times.haml index bb0975a9535..bb0975a9535 100644 --- a/app/views/projects/graphs/ci/_build_times.haml +++ b/app/views/projects/pipelines/charts/_build_times.haml diff --git a/app/views/projects/graphs/ci/_builds.haml b/app/views/projects/pipelines/charts/_builds.haml index b6f453b9736..b6f453b9736 100644 --- a/app/views/projects/graphs/ci/_builds.haml +++ b/app/views/projects/pipelines/charts/_builds.haml diff --git a/app/views/projects/graphs/ci/_overall.haml b/app/views/projects/pipelines/charts/_overall.haml index edc4f7b079f..edc4f7b079f 100644 --- a/app/views/projects/graphs/ci/_overall.haml +++ b/app/views/projects/pipelines/charts/_overall.haml diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 4147a617d95..acb61aa2490 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -48,28 +48,6 @@ = link_to ci_lint_path, class: 'btn btn-default' do %span CI Lint .content-list.pipelines{ data: { url: namespace_project_pipelines_path(@project.namespace, @project, format: :json) } } - .pipeline-svgs{ "data" => {"commit_icon_svg" => custom_icon("icon_commit"), - "icon_status_canceled" => custom_icon("icon_status_canceled"), - "icon_status_running" => custom_icon("icon_status_running"), - "icon_status_skipped" => custom_icon("icon_status_skipped"), - "icon_status_created" => custom_icon("icon_status_created"), - "icon_status_pending" => custom_icon("icon_status_pending"), - "icon_status_success" => custom_icon("icon_status_success"), - "icon_status_failed" => custom_icon("icon_status_failed"), - "icon_status_warning" => custom_icon("icon_status_warning"), - "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"), - "stage_icon_status_running" => custom_icon("icon_status_running_borderless"), - "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"), - "stage_icon_status_created" => custom_icon("icon_status_created_borderless"), - "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"), - "stage_icon_status_success" => custom_icon("icon_status_success_borderless"), - "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"), - "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"), - "icon_play" => custom_icon("icon_play"), - "icon_timer" => custom_icon("icon_timer"), - "icon_status_manual" => custom_icon("icon_status_manual"), - } } - - .vue-pipelines-index + .vue-pipelines-index = page_specific_javascript_bundle_tag('vue_pipelines') diff --git a/app/views/projects/show.atom.builder b/app/views/projects/show.atom.builder index 11310d5e1e1..5c7f2e315f0 100644 --- a/app/views/projects/show.atom.builder +++ b/app/views/projects/show.atom.builder @@ -1,7 +1,7 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do xml.title "#{@project.name} activity" - xml.link href: namespace_project_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" + xml.link href: namespace_project_url(@project.namespace, @project, rss_url_options), rel: "self", type: "application/atom+xml" xml.link href: namespace_project_url(@project.namespace, @project), rel: "alternate", type: "text/html" xml.id namespace_project_url(@project.namespace, @project) xml.updated @events[0].updated_at.xmlschema if @events[0] diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 80d4081dd7b..30d185e6556 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -1,15 +1,15 @@ - @no_container = true = content_for :meta_tags do - - if current_user - = auto_discovery_link_tag(:atom, namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "#{@project.name} activity") + = auto_discovery_link_tag(:atom, namespace_project_path(@project.namespace, @project, rss_url_options), title: "#{@project.name} activity") = content_for :flash_message do - if current_user && can?(current_user, :download_code, @project) = render 'shared/no_ssh' = render 'shared/no_password' -= render 'projects/last_push' += render "projects/head" += render "projects/last_push" = render "home_panel" - if current_user && can?(current_user, :download_code, @project) diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index e2f132f7742..7f9a44e565f 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -3,7 +3,7 @@ = render "projects/commits/head" .flex-list{ class: container_class } - .top-area.flex-row + .top-area.adjust .nav-text.row-main-content Tags give the ability to mark specific points in history as being important diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index 9864be3562a..a2a26039220 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -2,8 +2,7 @@ - page_title @path.presence || "Files", @ref = content_for :meta_tags do - - if current_user - = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits") + = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits") = render "projects/commits/head" = render 'projects/last_push' diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg index 9b67422da2c..10e6c49ae9f 100644 --- a/app/views/shared/_logo.svg +++ b/app/views/shared/_logo.svg @@ -1,4 +1,4 @@ -<svg width="36" height="36" class="tanuki-logo"> +<svg width="28" height="28" class="tanuki-logo" viewBox="0 0 36 36"> <path class="tanuki-shape tanuki-left-ear" fill="#e24329" d="M2 14l9.38 9v-9l-4-12.28c-.205-.632-1.176-.632-1.38 0z"/> <path class="tanuki-shape tanuki-right-ear" fill="#e24329" d="M34 14l-9.38 9v-9l4-12.28c.205-.632 1.176-.632 1.38 0z"/> <path class="tanuki-shape tanuki-nose" fill="#e24329" d="M18,34.38 3,14 33,14 Z"/> diff --git a/app/views/shared/groups/_dropdown.html.haml b/app/views/shared/groups/_dropdown.html.haml new file mode 100644 index 00000000000..37589b634fa --- /dev/null +++ b/app/views/shared/groups/_dropdown.html.haml @@ -0,0 +1,18 @@ +.dropdown.inline + %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' } + %span.light + - if @sort.present? + = sort_options_hash[@sort] + - else + = sort_title_recently_created + = icon('chevron-down') + %ul.dropdown-menu.dropdown-menu-align-right + %li + = link_to filter_groups_path(sort: sort_value_recently_created) do + = sort_title_recently_created + = link_to filter_groups_path(sort: sort_value_oldest_created) do + = sort_title_oldest_created + = link_to filter_groups_path(sort: sort_value_recently_updated) do + = sort_title_recently_updated + = link_to filter_groups_path(sort: sort_value_oldest_updated) do + = sort_title_oldest_updated diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 6c730e16f67..37a0c63e514 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -9,16 +9,16 @@ - if current_user %span.issuable-header-text.hide-collapsed.pull-left Todo - %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", aria: { label: "Toggle sidebar" } } + %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" } = sidebar_gutter_toggle_icon - if current_user - %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", aria: { label: (todo.nil? ? "Add todo" : "Mark done") }, data: { todo_text: "Add todo", mark_text: "Mark done", issuable_id: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project), delete_path: (dashboard_todo_path(todo) if todo) } } + %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", "aria-label" => (todo.nil? ? "Add todo" : "Mark done"), data: { todo_text: "Add todo", mark_text: "Mark done", issuable_id: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project), delete_path: (dashboard_todo_path(todo) if todo) } } %span.js-issuable-todo-text - if todo Mark done - else Add todo - = icon('spin spinner', class: 'hidden js-issuable-todo-loading') + = icon('spin spinner', class: 'hidden js-issuable-todo-loading', 'aria-hidden': 'true') = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f| .block.assignee @@ -26,10 +26,10 @@ - if issuable.assignee = link_to_member(@project, issuable.assignee, size: 24) - else - = icon('user') + = icon('user', 'aria-hidden': 'true') .title.hide-collapsed Assignee - = icon('spinner spin', class: 'block-loading') + = icon('spinner spin', class: 'block-loading', 'aria-hidden': 'true') - if can_edit_issuable = link_to 'Edit', '#', class: 'edit-link pull-right' .value.hide-collapsed @@ -37,7 +37,7 @@ = link_to_member(@project, issuable.assignee, size: 32, extra_class: 'bold') do - if issuable.instance_of?(MergeRequest) && !issuable.can_be_merged_by?(issuable.assignee) %span.pull-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: 'Not allowed to merge' } - = icon('exclamation-triangle') + = icon('exclamation-triangle', 'aria-hidden': 'true') %span.username = issuable.assignee.to_reference - else @@ -54,7 +54,7 @@ .block.milestone .sidebar-collapsed-icon - = icon('clock-o') + = icon('clock-o', 'aria-hidden': 'true') %span - if issuable.milestone %span.has-tooltip{ title: milestone_remaining_days(issuable.milestone), data: { container: 'body', html: 1, placement: 'left' } } @@ -63,7 +63,7 @@ None .title.hide-collapsed Milestone - = icon('spinner spin', class: 'block-loading') + = icon('spinner spin', class: 'block-loading', 'aria-hidden': 'true') - if can_edit_issuable = link_to 'Edit', '#', class: 'edit-link pull-right' .value.hide-collapsed @@ -77,20 +77,20 @@ = dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }}) - if issuable.has_attribute?(:time_estimate) #issuable-time-tracker.block - %issuable-time-tracker{ ':time_estimate' => 'issuable.time_estimate', ':time_spent' => 'issuable.total_time_spent', ':human_time_estimate' => 'issuable.human_time_estimate', ':human_time_spent' => 'issuable.human_total_time_spent', 'stopwatch-svg' => custom_icon('icon_stopwatch'), 'docs-url' => help_page_path('workflow/time_tracking.md') } + %issuable-time-tracker{ ':time_estimate' => 'issuable.time_estimate', ':time_spent' => 'issuable.total_time_spent', ':human_time_estimate' => 'issuable.human_time_estimate', ':human_time_spent' => 'issuable.human_total_time_spent', 'docs-url' => help_page_path('workflow/time_tracking.md') } // Fallback while content is loading .title.hide-collapsed Time tracking - = icon('spinner spin') + = icon('spinner spin', 'aria-hidden': 'true') - if issuable.has_attribute?(:due_date) .block.due_date .sidebar-collapsed-icon - = icon('calendar') + = icon('calendar', 'aria-hidden': 'true') %span.js-due-date-sidebar-value = issuable.due_date.try(:to_s, :medium) || 'None' .title.hide-collapsed Due date - = icon('spinner spin', class: 'block-loading') + = icon('spinner spin', class: 'block-loading', 'aria-hidden': 'true') - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) = link_to 'Edit', '#', class: 'edit-link pull-right' .value.hide-collapsed @@ -110,7 +110,7 @@ .dropdown %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable.to_ability_name}[due_date]", ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable) } } %span.dropdown-toggle-text Due date - = icon('chevron-down') + = icon('chevron-down', 'aria-hidden': 'true') .dropdown-menu.dropdown-menu-due-date = dropdown_title('Due date') = dropdown_content do @@ -120,12 +120,12 @@ - selected_labels = issuable.labels .block.labels .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } } - = icon('tags') + = icon('tags', 'aria-hidden': 'true') %span = selected_labels.size .title.hide-collapsed Labels - = icon('spinner spin', class: 'block-loading') + = icon('spinner spin', class: 'block-loading', 'aria-hidden': 'true') - if can_edit_issuable = link_to 'Edit', '#', class: 'edit-link pull-right' .value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) } @@ -141,7 +141,7 @@ %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project) } } %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) } = multi_label_name(selected_labels, "Labels") - = icon('chevron-down') + = icon('chevron-down', 'aria-hidden': 'true') .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable = render partial: "shared/issuable/label_page_default" - if can? current_user, :admin_label, @project and @project @@ -152,7 +152,7 @@ - subscribed = issuable.subscribed?(current_user, @project) .block.light.subscription{ data: { url: toggle_subscription_path(issuable) } } .sidebar-collapsed-icon - = icon('rss') + = icon('rss', 'aria-hidden': 'true') %span.issuable-header-text.hide-collapsed.pull-left Notifications - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed' diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml index d27fba805a3..78079f633d5 100644 --- a/app/views/shared/milestones/_summary.html.haml +++ b/app/views/shared/milestones/_summary.html.haml @@ -6,14 +6,15 @@ .milestone-stats-and-buttons .milestone-stats - %span.milestone-stat.with-drilldown - %strong= milestone.issues_visible_to_user(current_user).size - issues: - %span.milestone-stat - %strong= milestone.issues_visible_to_user(current_user).opened.size - open and - %strong= milestone.issues_visible_to_user(current_user).closed.size - closed + - if !project || can?(current_user, :read_issue, project) + %span.milestone-stat.with-drilldown + %strong= milestone.issues_visible_to_user(current_user).size + issues: + %span.milestone-stat + %strong= milestone.issues_visible_to_user(current_user).opened.size + open and + %strong= milestone.issues_visible_to_user(current_user).closed.size + closed %span.milestone-stat.with-drilldown %strong= milestone.merge_requests.size merge requests: @@ -32,10 +33,12 @@ .milestone-progress-buttons %span.tab-issues-buttons - - if project && can?(current_user, :create_issue, project) - = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn", title: "New Issue" do - New Issue - = link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn" + - if project + - if can?(current_user, :create_issue, project) + = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn", title: "New Issue" do + New Issue + - if can?(current_user, :read_issue, project) + = link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn" %span.tab-merge-requests-buttons.hidden = link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn" diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml index c8f2319d95a..a0e9ec46220 100644 --- a/app/views/shared/milestones/_tabs.html.haml +++ b/app/views/shared/milestones/_tabs.html.haml @@ -1,12 +1,18 @@ %ul.nav-links.no-top.no-bottom - %li.active - = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do - Issues - %span.badge= milestone.issues_visible_to_user(current_user).size - %li - = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do - Merge Requests - %span.badge= milestone.merge_requests.size + - if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project) + %li.active + = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do + Issues + %span.badge= milestone.issues_visible_to_user(current_user).size + %li + = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do + Merge Requests + %span.badge= milestone.merge_requests.size + - else + %li.active + = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do + Merge Requests + %span.badge= milestone.merge_requests.size %li = link_to '#tab-participants', 'data-toggle' => 'tab' do Participants @@ -20,10 +26,14 @@ - show_full_project_name = local_assigns.fetch(:show_full_project_name, false) .tab-content.milestone-content - .tab-pane.active#tab-issues - = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user).include_associations, show_project_name: show_project_name, show_full_project_name: show_full_project_name - .tab-pane#tab-merge-requests - = render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name + - if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project) + .tab-pane.active#tab-issues + = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user).include_associations, show_project_name: show_project_name, show_full_project_name: show_full_project_name + .tab-pane#tab-merge-requests + = render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name + - else + .tab-pane.active#tab-merge-requests + = render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name .tab-pane#tab-participants = render 'shared/milestones/participants_tab', users: milestone.participants .tab-pane#tab-labels diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index c130f3d9e17..af091f9ab88 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -24,13 +24,12 @@ = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray', title: 'Report abuse', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do = icon('exclamation-circle') - - if current_user - = link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do - = icon('rss') - - if current_user.admin? - = link_to [:admin, @user], class: 'btn btn-gray', title: 'View user in admin area', - data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = icon('users') + = link_to user_path(@user, rss_url_options), class: 'btn btn-gray' do + = icon('rss') + - if current_user && current_user.admin? + = link_to [:admin, @user], class: 'btn btn-gray', title: 'View user in admin area', + data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = icon('users') .profile-header .avatar-holder diff --git a/changelogs/unreleased/19497-hide-relevant-info-when-project-issues-are-disabled.yml b/changelogs/unreleased/19497-hide-relevant-info-when-project-issues-are-disabled.yml new file mode 100644 index 00000000000..eceb2b9fac6 --- /dev/null +++ b/changelogs/unreleased/19497-hide-relevant-info-when-project-issues-are-disabled.yml @@ -0,0 +1,4 @@ +--- +title: Hide issue info when project issues are disabled +merge_request: +author: George Andrinopoulos diff --git a/changelogs/unreleased/22951-fix-todos-api-endpoint-error-for-commits.yml b/changelogs/unreleased/22951-fix-todos-api-endpoint-error-for-commits.yml new file mode 100644 index 00000000000..a53e7d77c16 --- /dev/null +++ b/changelogs/unreleased/22951-fix-todos-api-endpoint-error-for-commits.yml @@ -0,0 +1,4 @@ +--- +title: Add spec for todo with target_type Commit +merge_request: 9351 +author: George Andrinopoulos diff --git a/changelogs/unreleased/25437-just-emoji.yml b/changelogs/unreleased/25437-just-emoji.yml new file mode 100644 index 00000000000..ceb81a47f2d --- /dev/null +++ b/changelogs/unreleased/25437-just-emoji.yml @@ -0,0 +1,4 @@ +--- +title: Introduce /award slash command; Allow posting of just an emoji in comment +merge_request: 9382 +author: mhasbini diff --git a/changelogs/unreleased/25920-create-issue-from-failing-build.yml b/changelogs/unreleased/25920-create-issue-from-failing-build.yml new file mode 100644 index 00000000000..580d1074aa7 --- /dev/null +++ b/changelogs/unreleased/25920-create-issue-from-failing-build.yml @@ -0,0 +1,4 @@ +--- +title: Add button to create issue for failing build +merge_request: 9391 +author: Alex Sanford diff --git a/changelogs/unreleased/26136-list-repository-tree-api-doc.yml b/changelogs/unreleased/26136-list-repository-tree-api-doc.yml new file mode 100644 index 00000000000..85d8bc6ca8a --- /dev/null +++ b/changelogs/unreleased/26136-list-repository-tree-api-doc.yml @@ -0,0 +1,4 @@ +--- +title: Make documentation of list repository tree API call more detailed +merge_request: 9532 +author: Marius Kleiner diff --git a/changelogs/unreleased/2629-show-public-rss-feeds-to-anonymous-users.yml b/changelogs/unreleased/2629-show-public-rss-feeds-to-anonymous-users.yml new file mode 100644 index 00000000000..6ee8e5724bc --- /dev/null +++ b/changelogs/unreleased/2629-show-public-rss-feeds-to-anonymous-users.yml @@ -0,0 +1,4 @@ +--- +title: Show public RSS feeds to anonymous users +merge_request: 9596 +author: Michael Kozono diff --git a/changelogs/unreleased/26348-cleanup-navigation-order.yml b/changelogs/unreleased/26348-cleanup-navigation-order.yml new file mode 100644 index 00000000000..d5324f9e025 --- /dev/null +++ b/changelogs/unreleased/26348-cleanup-navigation-order.yml @@ -0,0 +1,4 @@ +--- +title: Clean-up Project navigation order +merge_request: 9272 +author: diff --git a/changelogs/unreleased/27142-api-replace-destroy-with-stop-environment.yml b/changelogs/unreleased/27142-api-replace-destroy-with-stop-environment.yml new file mode 100644 index 00000000000..ee236310a71 --- /dev/null +++ b/changelogs/unreleased/27142-api-replace-destroy-with-stop-environment.yml @@ -0,0 +1,4 @@ +--- +title: API: Add environment stop action +merge_request: 8808 +author: diff --git a/changelogs/unreleased/27267-events-project-query-performance-regression.yml b/changelogs/unreleased/27267-events-project-query-performance-regression.yml deleted file mode 100644 index a1697b57eac..00000000000 --- a/changelogs/unreleased/27267-events-project-query-performance-regression.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Add performance query regression fix for !9088 affecting #27267' -merge_request: -author: diff --git a/changelogs/unreleased/27354-navigation-new-button.yml b/changelogs/unreleased/27354-navigation-new-button.yml new file mode 100644 index 00000000000..62cac9bbbd3 --- /dev/null +++ b/changelogs/unreleased/27354-navigation-new-button.yml @@ -0,0 +1,4 @@ +--- +title: Re-add the New Project button in nav bar +merge_request: +author: diff --git a/changelogs/unreleased/27501-api-use-visibility-everywhere.yml b/changelogs/unreleased/27501-api-use-visibility-everywhere.yml new file mode 100644 index 00000000000..f1b70687878 --- /dev/null +++ b/changelogs/unreleased/27501-api-use-visibility-everywhere.yml @@ -0,0 +1,4 @@ +--- +title: "API: Use `visibility` as string parameter everywhere" +merge_request: 9337 +author: diff --git a/changelogs/unreleased/27532_api_changes.yml b/changelogs/unreleased/27532_api_changes.yml new file mode 100644 index 00000000000..778469d5a86 --- /dev/null +++ b/changelogs/unreleased/27532_api_changes.yml @@ -0,0 +1,4 @@ +--- +title: Use iids as filter parameter +merge_request: 9096 +author: diff --git a/changelogs/unreleased/27778-a11y-sidebar.yml b/changelogs/unreleased/27778-a11y-sidebar.yml new file mode 100644 index 00000000000..fb37d7fdb35 --- /dev/null +++ b/changelogs/unreleased/27778-a11y-sidebar.yml @@ -0,0 +1,5 @@ +--- +title: Improves a11y in sidebar by adding aria-hidden attributes in i tags and by + fixing two broken aria-hidden attributes +merge_request: +author: diff --git a/changelogs/unreleased/27934-left-align-logo.yml b/changelogs/unreleased/27934-left-align-logo.yml new file mode 100644 index 00000000000..d4e5e169465 --- /dev/null +++ b/changelogs/unreleased/27934-left-align-logo.yml @@ -0,0 +1,4 @@ +--- +title: Left align logo +merge_request: +author: diff --git a/changelogs/unreleased/27978-improve-task-list-ux.yml b/changelogs/unreleased/27978-improve-task-list-ux.yml new file mode 100644 index 00000000000..a6bd99da82e --- /dev/null +++ b/changelogs/unreleased/27978-improve-task-list-ux.yml @@ -0,0 +1,4 @@ +--- +title: Only add a newline in the Markdown Editor if the current line is not empty +merge_request: 9455 +author: Jan Christophersen diff --git a/changelogs/unreleased/27989-disable-counting-tags.yml b/changelogs/unreleased/27989-disable-counting-tags.yml deleted file mode 100644 index 988785ac454..00000000000 --- a/changelogs/unreleased/27989-disable-counting-tags.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Disable unused tags count cache for Projects, Builds and Runners -merge_request: -author: diff --git a/changelogs/unreleased/28010-mr-merge-button-default-to-danger.yml b/changelogs/unreleased/28010-mr-merge-button-default-to-danger.yml new file mode 100644 index 00000000000..06bb669ceac --- /dev/null +++ b/changelogs/unreleased/28010-mr-merge-button-default-to-danger.yml @@ -0,0 +1,4 @@ +--- +title: Default to subtle MR mege button until CI status is available +merge_request: +author: diff --git a/changelogs/unreleased/28093-snippet-and-issue-spam-check-on-edit.yml b/changelogs/unreleased/28093-snippet-and-issue-spam-check-on-edit.yml deleted file mode 100644 index d70b5ef8fd5..00000000000 --- a/changelogs/unreleased/28093-snippet-and-issue-spam-check-on-edit.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Spam check and reCAPTCHA improvements -merge_request: -author: diff --git a/changelogs/unreleased/28212-avoid-dos-on-build-trace.yml b/changelogs/unreleased/28212-avoid-dos-on-build-trace.yml deleted file mode 100644 index 800e0389c86..00000000000 --- a/changelogs/unreleased/28212-avoid-dos-on-build-trace.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Replace setInterval with setTimeout to prevent highly frequent requests -merge_request: 9271 -author: Takuya Noguchi diff --git a/changelogs/unreleased/28257-issues-iids.yml b/changelogs/unreleased/28257-issues-iids.yml new file mode 100644 index 00000000000..0a85504a8de --- /dev/null +++ b/changelogs/unreleased/28257-issues-iids.yml @@ -0,0 +1,4 @@ +--- +title: API issues - support filtering by iids +merge_request: +author: diff --git a/changelogs/unreleased/28357-colon-search.yml b/changelogs/unreleased/28357-colon-search.yml deleted file mode 100644 index 4bbb0dc12b2..00000000000 --- a/changelogs/unreleased/28357-colon-search.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow searching issues for strings containing colons -merge_request: -author: diff --git a/changelogs/unreleased/28410-dropdown-styling.yml b/changelogs/unreleased/28410-dropdown-styling.yml new file mode 100644 index 00000000000..2a7af1dd6e8 --- /dev/null +++ b/changelogs/unreleased/28410-dropdown-styling.yml @@ -0,0 +1,4 @@ +--- +title: Add badges to global dropdown +merge_request: +author: diff --git a/changelogs/unreleased/28609-fix-redirect-to-home-page-url.yml b/changelogs/unreleased/28609-fix-redirect-to-home-page-url.yml new file mode 100644 index 00000000000..baf832d4495 --- /dev/null +++ b/changelogs/unreleased/28609-fix-redirect-to-home-page-url.yml @@ -0,0 +1,4 @@ +--- +title: Fix the redirect to custom home page URL +merge_request: 9518 +author: diff --git a/changelogs/unreleased/28696-improve-grammar-gitlab-flow-doc.yml b/changelogs/unreleased/28696-improve-grammar-gitlab-flow-doc.yml new file mode 100644 index 00000000000..e38e5d0db5b --- /dev/null +++ b/changelogs/unreleased/28696-improve-grammar-gitlab-flow-doc.yml @@ -0,0 +1,4 @@ +--- +title: Improve grammar in GitLab flow documentation +merge_request: 9552 +author: infogrind diff --git a/changelogs/unreleased/28805-download-archive-with-branch-like-feature-xxxx-add-extra-directory-level.yml b/changelogs/unreleased/28805-download-archive-with-branch-like-feature-xxxx-add-extra-directory-level.yml new file mode 100644 index 00000000000..38ff6b97b2b --- /dev/null +++ b/changelogs/unreleased/28805-download-archive-with-branch-like-feature-xxxx-add-extra-directory-level.yml @@ -0,0 +1,4 @@ +--- +title: Ensure archive download is only one directory deep +merge_request: 9616 +author: diff --git a/changelogs/unreleased/28807-search-for-milestone-by-title-in-rest-api.yml b/changelogs/unreleased/28807-search-for-milestone-by-title-in-rest-api.yml new file mode 100644 index 00000000000..0016253e32e --- /dev/null +++ b/changelogs/unreleased/28807-search-for-milestone-by-title-in-rest-api.yml @@ -0,0 +1,4 @@ +--- +title: Enable filtering milestones by search criteria in the API +merge_request: 9606 +author: diff --git a/changelogs/unreleased/28837-remove-help-duplicate.yml b/changelogs/unreleased/28837-remove-help-duplicate.yml new file mode 100644 index 00000000000..b1001245663 --- /dev/null +++ b/changelogs/unreleased/28837-remove-help-duplicate.yml @@ -0,0 +1,4 @@ +--- +title: Remove help link from right dropdown +merge_request: +author: diff --git a/changelogs/unreleased/28850-fix-broken-migration.yml b/changelogs/unreleased/28850-fix-broken-migration.yml new file mode 100644 index 00000000000..7f59a7708bc --- /dev/null +++ b/changelogs/unreleased/28850-fix-broken-migration.yml @@ -0,0 +1,4 @@ +--- +title: Fix broken migration when upgrading straight to 8.17.1 +merge_request: 9613 +author: diff --git a/changelogs/unreleased/28865-filter-by-authorized-projects-in-v4.yml b/changelogs/unreleased/28865-filter-by-authorized-projects-in-v4.yml new file mode 100644 index 00000000000..7c64783cbd0 --- /dev/null +++ b/changelogs/unreleased/28865-filter-by-authorized-projects-in-v4.yml @@ -0,0 +1,4 @@ +--- +title: Add filter param for project membership for current_user in API v4 +merge_request: +author: diff --git a/changelogs/unreleased/28893-highlighted-diff-doesn-t-stay-highlighted-on-refresh.yml b/changelogs/unreleased/28893-highlighted-diff-doesn-t-stay-highlighted-on-refresh.yml new file mode 100644 index 00000000000..9ba33af010c --- /dev/null +++ b/changelogs/unreleased/28893-highlighted-diff-doesn-t-stay-highlighted-on-refresh.yml @@ -0,0 +1,4 @@ +--- +title: Highlight line number if specified on diff pages when page loads +merge_request: 9664 +author: diff --git a/changelogs/unreleased/28935-make-logo-smaller.yml b/changelogs/unreleased/28935-make-logo-smaller.yml new file mode 100644 index 00000000000..ef79fc7d212 --- /dev/null +++ b/changelogs/unreleased/28935-make-logo-smaller.yml @@ -0,0 +1,4 @@ +--- +title: Decrease tanuki logo size +merge_request: +author: diff --git a/changelogs/unreleased/3440-remove-hsts-header.yml b/changelogs/unreleased/3440-remove-hsts-header.yml new file mode 100644 index 00000000000..0310e733f4e --- /dev/null +++ b/changelogs/unreleased/3440-remove-hsts-header.yml @@ -0,0 +1,4 @@ +--- +title: Stop setting Strict-Transport-Securty header from within the app +merge_request: +author: diff --git a/changelogs/unreleased/6073_project_api.yml b/changelogs/unreleased/6073_project_api.yml new file mode 100644 index 00000000000..fd6792a406e --- /dev/null +++ b/changelogs/unreleased/6073_project_api.yml @@ -0,0 +1,4 @@ +--- +title: 'API project create: Make name or path required' +merge_request: 9416 +author: diff --git a/changelogs/unreleased/add-issues-tooltip.yml b/changelogs/unreleased/add-issues-tooltip.yml deleted file mode 100644 index 58adb6c6b5a..00000000000 --- a/changelogs/unreleased/add-issues-tooltip.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Disabled tooltip on add issues button in usse boards -merge_request: -author: diff --git a/changelogs/unreleased/add-kube-ca-pem-file-deprecate-kube-ca-pem.yml b/changelogs/unreleased/add-kube-ca-pem-file-deprecate-kube-ca-pem.yml new file mode 100644 index 00000000000..1ae1e3c7a7a --- /dev/null +++ b/changelogs/unreleased/add-kube-ca-pem-file-deprecate-kube-ca-pem.yml @@ -0,0 +1,4 @@ +--- +title: Add KUBE_CA_PEM_FILE, deprecate KUBE_CA_PEM +merge_request: 9398 +author: diff --git a/changelogs/unreleased/api-empty-return.yml b/changelogs/unreleased/api-empty-return.yml new file mode 100644 index 00000000000..7810e83eb0e --- /dev/null +++ b/changelogs/unreleased/api-empty-return.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Return 204 for all delete endpoints' +merge_request: 9397 +author: Robert Schilling diff --git a/changelogs/unreleased/commit-search-ui-fix.yml b/changelogs/unreleased/commit-search-ui-fix.yml deleted file mode 100644 index 4a5c2cf6090..00000000000 --- a/changelogs/unreleased/commit-search-ui-fix.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed commit search UI -merge_request: -author: diff --git a/changelogs/unreleased/diff-make-obvious-cant-comment.yml b/changelogs/unreleased/diff-make-obvious-cant-comment.yml new file mode 100644 index 00000000000..2cb95947939 --- /dev/null +++ b/changelogs/unreleased/diff-make-obvious-cant-comment.yml @@ -0,0 +1,4 @@ +--- +title: Visually show expanded diff lines cant have comments +merge_request: +author: diff --git a/changelogs/unreleased/dm-dont-copy-toolip.yml b/changelogs/unreleased/dm-dont-copy-toolip.yml new file mode 100644 index 00000000000..2b134da66ab --- /dev/null +++ b/changelogs/unreleased/dm-dont-copy-toolip.yml @@ -0,0 +1,4 @@ +--- +title: Don't copy tooltip when copying GFM +merge_request: +author: diff --git a/changelogs/unreleased/dm-fix-api-create-file-on-empty-repo.yml b/changelogs/unreleased/dm-fix-api-create-file-on-empty-repo.yml new file mode 100644 index 00000000000..7ac25c0a83e --- /dev/null +++ b/changelogs/unreleased/dm-fix-api-create-file-on-empty-repo.yml @@ -0,0 +1,4 @@ +--- +title: Fix creating a file in an empty repo using the API +merge_request: 9632 +author: diff --git a/changelogs/unreleased/dm-fix-cherry-pick.yml b/changelogs/unreleased/dm-fix-cherry-pick.yml new file mode 100644 index 00000000000..e924b821d7e --- /dev/null +++ b/changelogs/unreleased/dm-fix-cherry-pick.yml @@ -0,0 +1,4 @@ +--- +title: Fix cherry-picking or reverting through an MR +merge_request: +author: diff --git a/changelogs/unreleased/dm-group-reference-full-name.yml b/changelogs/unreleased/dm-group-reference-full-name.yml new file mode 100644 index 00000000000..f445d955529 --- /dev/null +++ b/changelogs/unreleased/dm-group-reference-full-name.yml @@ -0,0 +1,4 @@ +--- +title: Use full group name in GFM group reference title +merge_request: +author: diff --git a/changelogs/unreleased/dz-dashboard-groups-search.yml b/changelogs/unreleased/dz-dashboard-groups-search.yml new file mode 100644 index 00000000000..c473cba774d --- /dev/null +++ b/changelogs/unreleased/dz-dashboard-groups-search.yml @@ -0,0 +1,4 @@ +--- +title: Add filter and sorting to dashboard groups page +merge_request: 9619 +author: diff --git a/changelogs/unreleased/expose-pagination-headers.yml b/changelogs/unreleased/expose-pagination-headers.yml new file mode 100644 index 00000000000..1b4cd43fa06 --- /dev/null +++ b/changelogs/unreleased/expose-pagination-headers.yml @@ -0,0 +1,4 @@ +--- +title: 'CORS: Whitelist pagination headers' +merge_request: 9651 +author: Robert Schilling diff --git a/changelogs/unreleased/fix-gb-remove-deprecated-ci-build-status-badge.yml b/changelogs/unreleased/fix-gb-remove-deprecated-ci-build-status-badge.yml new file mode 100644 index 00000000000..71ff768a190 --- /dev/null +++ b/changelogs/unreleased/fix-gb-remove-deprecated-ci-build-status-badge.yml @@ -0,0 +1,4 @@ +--- +title: Remove deprecated build status badge and related services +merge_request: 9620 +author: diff --git a/changelogs/unreleased/fix-gb-update-commit-status-api.yml b/changelogs/unreleased/fix-gb-update-commit-status-api.yml new file mode 100644 index 00000000000..aa4fcba4e89 --- /dev/null +++ b/changelogs/unreleased/fix-gb-update-commit-status-api.yml @@ -0,0 +1,4 @@ +--- +title: Fix updaing commit status when using optional attributes +merge_request: 9618 +author: diff --git a/changelogs/unreleased/gitaly-post-receive.yml b/changelogs/unreleased/gitaly-post-receive.yml new file mode 100644 index 00000000000..cf206e39084 --- /dev/null +++ b/changelogs/unreleased/gitaly-post-receive.yml @@ -0,0 +1,4 @@ +--- +title: Add internal API to notify Gitaly of post receive +merge_request: 8983 +author: diff --git a/changelogs/unreleased/issue-descrpiption-spinner-off.yml b/changelogs/unreleased/issue-descrpiption-spinner-off.yml new file mode 100644 index 00000000000..87104d09804 --- /dev/null +++ b/changelogs/unreleased/issue-descrpiption-spinner-off.yml @@ -0,0 +1,4 @@ +--- +title: Fixed loading spinner position on issue template toggle +merge_request: +author: diff --git a/changelogs/unreleased/issue-tags-layout.yml b/changelogs/unreleased/issue-tags-layout.yml new file mode 100644 index 00000000000..abf4a609932 --- /dev/null +++ b/changelogs/unreleased/issue-tags-layout.yml @@ -0,0 +1,4 @@ +--- +title: Fix 'New Tag' layout on Tags page +merge_request: +author: Robert Marcano diff --git a/changelogs/unreleased/issue_25112.yml b/changelogs/unreleased/issue_25112.yml deleted file mode 100644 index c43d2732b9a..00000000000 --- a/changelogs/unreleased/issue_25112.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Disable invalid service templates -merge_request: -author: diff --git a/changelogs/unreleased/issue_28051_2.yml b/changelogs/unreleased/issue_28051_2.yml deleted file mode 100644 index 8cc32ad8493..00000000000 --- a/changelogs/unreleased/issue_28051_2.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Use default branch as target_branch when parameter is missing -merge_request: -author: diff --git a/changelogs/unreleased/long-file-name-overflow.yml b/changelogs/unreleased/long-file-name-overflow.yml new file mode 100644 index 00000000000..7ccf05491e1 --- /dev/null +++ b/changelogs/unreleased/long-file-name-overflow.yml @@ -0,0 +1,4 @@ +--- +title: Fixed long file names overflowing under action buttons +merge_request: +author: diff --git a/changelogs/unreleased/only-create-unmergeable-todo-once.yml b/changelogs/unreleased/only-create-unmergeable-todo-once.yml new file mode 100644 index 00000000000..e675ed945ad --- /dev/null +++ b/changelogs/unreleased/only-create-unmergeable-todo-once.yml @@ -0,0 +1,4 @@ +--- +title: Only create unmergeable todos once when MR fails to merge +merge_request: +author: diff --git a/changelogs/unreleased/pages-0-3-2.yml b/changelogs/unreleased/pages-0-3-2.yml deleted file mode 100644 index f660379f2e6..00000000000 --- a/changelogs/unreleased/pages-0-3-2.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Upgrade GitLab Pages to v0.3.2 -merge_request: -author: diff --git a/changelogs/unreleased/remove-new-relic-gem.yml b/changelogs/unreleased/remove-new-relic-gem.yml new file mode 100644 index 00000000000..b15ecd3e4e7 --- /dev/null +++ b/changelogs/unreleased/remove-new-relic-gem.yml @@ -0,0 +1,4 @@ +--- +title: Remove the newrelic gem +merge_request: 9622 +author: Robert Schilling diff --git a/changelogs/unreleased/tooltip-hide-on-scroll.yml b/changelogs/unreleased/tooltip-hide-on-scroll.yml new file mode 100644 index 00000000000..cd81d303330 --- /dev/null +++ b/changelogs/unreleased/tooltip-hide-on-scroll.yml @@ -0,0 +1,4 @@ +--- +title: Fixed tooltip remaining after scrolling the page +merge_request: +author: diff --git a/changelogs/unreleased/use-v3-api-on-frontend.yml b/changelogs/unreleased/use-v3-api-on-frontend.yml new file mode 100644 index 00000000000..467ad3c8276 --- /dev/null +++ b/changelogs/unreleased/use-v3-api-on-frontend.yml @@ -0,0 +1,4 @@ +--- +title: Make projects dropdown only show projects you are a member of +merge_request: 9614 +author: diff --git a/changelogs/unreleased/user-calendar-border.yml b/changelogs/unreleased/user-calendar-border.yml new file mode 100644 index 00000000000..8ebcca83256 --- /dev/null +++ b/changelogs/unreleased/user-calendar-border.yml @@ -0,0 +1,4 @@ +--- +title: Removed top border from user contribution calendar +merge_request: +author: diff --git a/changelogs/unreleased/zj-fix-slash-command-labels.yml b/changelogs/unreleased/zj-fix-slash-command-labels.yml deleted file mode 100644 index 93b7194dd4e..00000000000 --- a/changelogs/unreleased/zj-fix-slash-command-labels.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Chat slash commands show labels correctly -merge_request: -author: diff --git a/config/application.rb b/config/application.rb index 9088d3c432b..45f3b20d214 100644 --- a/config/application.rb +++ b/config/application.rb @@ -120,7 +120,7 @@ module Gitlab credentials: true, headers: :any, methods: :any, - expose: ['Link'] + expose: ['Link', 'X-Total', 'X-Total-Pages', 'X-Per-Page', 'X-Page', 'X-Next-Page', 'X-Prev-Page'] end # Cross-origin requests must not have the session cookie available @@ -130,7 +130,7 @@ module Gitlab credentials: false, headers: :any, methods: :any, - expose: ['Link'] + expose: ['Link', 'X-Total', 'X-Total-Pages', 'X-Per-Page', 'X-Page', 'X-Next-Page', 'X-Prev-Page'] end end diff --git a/config/initializers/8_gitaly.rb b/config/initializers/8_gitaly.rb new file mode 100644 index 00000000000..07dd30f0a24 --- /dev/null +++ b/config/initializers/8_gitaly.rb @@ -0,0 +1,2 @@ +# Make sure we initialize a Gitaly channel before Sidekiq starts multi-threaded execution. +Gitlab::GitalyClient.channel unless Rails.env.test? diff --git a/config/newrelic.yml b/config/newrelic.yml deleted file mode 100644 index 9ef922a38d9..00000000000 --- a/config/newrelic.yml +++ /dev/null @@ -1,16 +0,0 @@ -# New Relic configuration file -# -# This file is here to make sure the New Relic gem stays -# quiet by default. -# -# To enable and configure New Relic, please use -# environment variables, e.g. NEW_RELIC_ENABLED=true - -production: - enabled: false - -development: - enabled: false - -test: - enabled: false diff --git a/config/routes/ci.rb b/config/routes/ci.rb index 47a049d5b20..8d23aa8fbf6 100644 --- a/config/routes/ci.rb +++ b/config/routes/ci.rb @@ -5,11 +5,5 @@ namespace :ci do resource :lint, only: [:show, :create] - resources :projects, only: [:index, :show] do - member do - get :status, to: 'projects#badge' - end - end - - root to: 'projects#index' + root to: redirect('/') end diff --git a/config/routes/project.rb b/config/routes/project.rb index 84f123ff717..2703bf4ab46 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -58,6 +58,7 @@ constraints(ProjectUrlConstrainer.new) do resources :graphs, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex } do member do + get :charts get :commits get :ci get :languages @@ -100,7 +101,7 @@ constraints(ProjectUrlConstrainer.new) do get :merge_check post :merge get :merge_widget_refresh - post :cancel_merge_when_build_succeeds + post :cancel_merge_when_pipeline_succeeds get :ci_status get :ci_environments_status post :toggle_subscription @@ -140,6 +141,7 @@ constraints(ProjectUrlConstrainer.new) do resources :pipelines, only: [:index, :new, :create, :show] do collection do resource :pipelines_settings, path: 'settings', only: [:show, :update] + get :charts end member do diff --git a/config/webpack.config.js b/config/webpack.config.js index a71ec0c5f52..13273902b0e 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -28,6 +28,7 @@ var config = { environments_folder: './environments/folder/environments_folder_bundle.js', filtered_search: './filtered_search/filtered_search_bundle.js', graphs: './graphs/graphs_bundle.js', + groups_list: './groups_list.js', issuable: './issuable/issuable_bundle.js', merge_conflicts: './merge_conflicts/merge_conflicts_bundle.js', merge_request_widget: './merge_request_widget/ci_bundle.js', @@ -63,6 +64,10 @@ var config = { 'stage-2' ] } + }, + { + test: /\.svg$/, + use: 'raw-loader' } ] }, @@ -86,6 +91,7 @@ var config = { '~': path.join(ROOT_PATH, 'app/assets/javascripts'), 'bootstrap/js': 'bootstrap-sass/assets/javascripts/bootstrap', 'emoji-aliases$': path.join(ROOT_PATH, 'fixtures/emojis/aliases.json'), + 'icons': path.join(ROOT_PATH, 'app/views/shared/icons'), 'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'), 'vue$': 'vue/dist/vue.common.js', } diff --git a/db/fixtures/development/13_comments.rb b/db/fixtures/development/13_comments.rb index 29b8081055d..bc2d74c8034 100644 --- a/db/fixtures/development/13_comments.rb +++ b/db/fixtures/development/13_comments.rb @@ -1,7 +1,7 @@ require './spec/support/sidekiq' Gitlab::Seeder.quiet do - Issue.all.each do |issue| + Issue.find_each do |issue| project = issue.project project.team.users.each do |user| @@ -16,7 +16,7 @@ Gitlab::Seeder.quiet do end end - MergeRequest.all.each do |mr| + MergeRequest.find_each do |mr| project = mr.project project.team.users.each do |user| diff --git a/db/migrate/20160610201627_migrate_users_notification_level.rb b/db/migrate/20160610201627_migrate_users_notification_level.rb index ce4f00e25fa..cd8b505de9f 100644 --- a/db/migrate/20160610201627_migrate_users_notification_level.rb +++ b/db/migrate/20160610201627_migrate_users_notification_level.rb @@ -1,4 +1,6 @@ class MigrateUsersNotificationLevel < ActiveRecord::Migration + DOWNTIME = false + # Migrates only users who changed their default notification level :participating # creating a new record on notification settings table diff --git a/db/migrate/20170217132157_rename_merge_when_build_succeeds.rb b/db/migrate/20170217132157_rename_merge_when_build_succeeds.rb new file mode 100644 index 00000000000..9011526565d --- /dev/null +++ b/db/migrate/20170217132157_rename_merge_when_build_succeeds.rb @@ -0,0 +1,29 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RenameMergeWhenBuildSucceeds < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = true + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + DOWNTIME_REASON = 'Renaming the column merge_when_build_succeeds' + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + rename_column :merge_requests, :merge_when_build_succeeds, :merge_when_pipeline_succeeds + end +end diff --git a/db/migrate/20170217151947_rename_only_allow_merge_if_build_succeeds.rb b/db/migrate/20170217151947_rename_only_allow_merge_if_build_succeeds.rb new file mode 100644 index 00000000000..b2b68ff72d1 --- /dev/null +++ b/db/migrate/20170217151947_rename_only_allow_merge_if_build_succeeds.rb @@ -0,0 +1,29 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RenameOnlyAllowMergeIfBuildSucceeds < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = true + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + DOWNTIME_REASON = 'Renaming the column only_allow_merge_if_build_succeeds' + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + rename_column :projects, :only_allow_merge_if_build_succeeds, :only_allow_merge_if_pipeline_succeeds + end +end diff --git a/db/post_migrate/20170211073944_disable_invalid_service_templates.rb b/db/post_migrate/20170211073944_disable_invalid_service_templates.rb index 84954b1ef64..603efc43782 100644 --- a/db/post_migrate/20170211073944_disable_invalid_service_templates.rb +++ b/db/post_migrate/20170211073944_disable_invalid_service_templates.rb @@ -1,10 +1,8 @@ class DisableInvalidServiceTemplates < ActiveRecord::Migration DOWNTIME = false - unless defined?(Service) - class Service < ActiveRecord::Base - self.inheritance_column = nil - end + class Service < ActiveRecord::Base + self.inheritance_column = nil end def up diff --git a/db/schema.rb b/db/schema.rb index 1d94368f66e..cd5aa339269 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170216141440) do +ActiveRecord::Schema.define(version: 20170217151947) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -689,7 +689,7 @@ ActiveRecord::Schema.define(version: 20170216141440) do t.integer "updated_by_id" t.text "merge_error" t.text "merge_params" - t.boolean "merge_when_build_succeeds", default: false, null: false + t.boolean "merge_when_pipeline_succeeds", default: false, null: false t.integer "merge_user_id" t.string "merge_commit_sha" t.datetime "deleted_at" @@ -972,7 +972,7 @@ ActiveRecord::Schema.define(version: 20170216141440) do t.boolean "last_repository_check_failed" t.datetime "last_repository_check_at" t.boolean "container_registry_enabled" - t.boolean "only_allow_merge_if_build_succeeds", default: false, null: false + t.boolean "only_allow_merge_if_pipeline_succeeds", default: false, null: false t.boolean "has_external_issue_tracker" t.string "repository_storage", default: "default", null: false t.boolean "request_access_enabled", default: false, null: false diff --git a/doc/api/README.md b/doc/api/README.md index b334ca46caf..3399e2bb5f6 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -89,7 +89,7 @@ You can use an OAuth 2 token to authenticate with the API by passing it either i Example of using the OAuth2 token in the header: ```shell -curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v3/projects +curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v4/projects ``` Read more about [GitLab as an OAuth2 client](oauth2.md). @@ -127,13 +127,13 @@ is defined in [`lib/api.rb`][lib-api-url]. Example of a valid API request: ```shell -GET https://gitlab.example.com/api/v3/projects?private_token=9koXpg98eAheJpvBs5tK +GET https://gitlab.example.com/api/v4/projects?private_token=9koXpg98eAheJpvBs5tK ``` Example of a valid API request using cURL and authentication via header: ```shell -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects" ``` The API uses JSON to serialize data. You don't need to specify `.json` at the @@ -159,6 +159,7 @@ The following table shows the possible return codes for API requests. | Return values | Description | | ------------- | ----------- | | `200 OK` | The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON. | +| `204 No Content` | The server has successfully fulfilled the request and that there is no additional content to send in the response payload body. | | `201 Created` | The `POST` request was successful and the resource is returned as JSON. | | `304 Not Modified` | Indicates that the resource has not been modified since the last request. | | `400 Bad Request` | A required attribute of the API request is missing, e.g., the title of an issue is not given. | @@ -206,7 +207,7 @@ GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=username ``` ```shell -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: username" "https://gitlab.example.com/api/v3/projects" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: username" "https://gitlab.example.com/api/v4/projects" ``` Example of a valid API call and a request using cURL with sudo request, @@ -217,7 +218,7 @@ GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=23 ``` ```shell -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: 23" "https://gitlab.example.com/api/v3/projects" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: 23" "https://gitlab.example.com/api/v4/projects" ``` ## Pagination @@ -233,7 +234,7 @@ resources you can pass the following parameters: In the example below, we list 50 [namespaces](namespaces.md) per page. ```bash -curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/namespaces?per_page=50 +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/namespaces?per_page=50 ``` ### Pagination Link header @@ -247,7 +248,7 @@ and we request the second page (`page=2`) of [comments](notes.md) of the issue with ID `8` which belongs to the project with ID `8`: ```bash -curl --head --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/8/issues/8/notes?per_page=3&page=2 +curl --head --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/8/issues/8/notes?per_page=3&page=2 ``` The response will then be: @@ -258,7 +259,7 @@ Cache-Control: no-cache Content-Length: 1103 Content-Type: application/json Date: Mon, 18 Jan 2016 09:43:18 GMT -Link: <https://gitlab.example.com/api/v3/projects/8/issues/8/notes?page=1&per_page=3>; rel="prev", <https://gitlab.example.com/api/v3/projects/8/issues/8/notes?page=3&per_page=3>; rel="next", <https://gitlab.example.com/api/v3/projects/8/issues/8/notes?page=1&per_page=3>; rel="first", <https://gitlab.example.com/api/v3/projects/8/issues/8/notes?page=3&per_page=3>; rel="last" +Link: <https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=1&per_page=3>; rel="prev", <https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=3&per_page=3>; rel="next", <https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=1&per_page=3>; rel="first", <https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=3&per_page=3>; rel="last" Status: 200 OK Vary: Origin X-Next-Page: 3 diff --git a/doc/api/access_requests.md b/doc/api/access_requests.md index dee3e384080..96b8d654c58 100644 --- a/doc/api/access_requests.md +++ b/doc/api/access_requests.md @@ -28,8 +28,8 @@ GET /projects/:id/access_requests | `id` | integer/string | yes | The group/project ID or path | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/access_requests +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/access_requests ``` Example response: @@ -69,8 +69,8 @@ POST /projects/:id/access_requests | `id` | integer/string | yes | The group/project ID or path | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/access_requests +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/access_requests ``` Example response: @@ -102,8 +102,8 @@ PUT /projects/:id/access_requests/:user_id/approve | `access_level` | integer | no | A valid access level (defaults: `30`, developer access level) | ```bash -curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests/:user_id/approve?access_level=20 -curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests/:user_id/approve?access_level=20 +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/access_requests/:user_id/approve?access_level=20 +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/access_requests/:user_id/approve?access_level=20 ``` Example response: @@ -134,6 +134,6 @@ DELETE /projects/:id/access_requests/:user_id | `user_id` | integer | yes | The user ID of the access requester | ```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests/:user_id -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests/:user_id +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/access_requests/:user_id +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/access_requests/:user_id ``` diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md index 58092bdd400..3470f8ce497 100644 --- a/doc/api/award_emoji.md +++ b/doc/api/award_emoji.md @@ -27,7 +27,7 @@ Parameters: | `awardable_id` | integer | yes | The ID of an awardable | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji ``` Example Response: @@ -88,7 +88,7 @@ Parameters: | `award_id` | integer | yes | The ID of the award emoji | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/1 +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji/1 ``` Example Response: @@ -131,7 +131,7 @@ Parameters: | `name` | string | yes | The name of the emoji, without colons | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji?name=blowfish +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji?name=blowfish ``` Example Response: @@ -175,28 +175,7 @@ Parameters: | `award_id` | integer | yes | The ID of a award_emoji | ```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/344 -``` - -Example Response: - -```json -{ - "id": 344, - "name": "blowfish", - "user": { - "name": "Administrator", - "username": "root", - "id": 1, - "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "web_url": "http://gitlab.example.com/root" - }, - "created_at": "2016-06-17T17:47:29.266Z", - "updated_at": "2016-06-17T17:47:29.266Z", - "awardable_id": 80, - "awardable_type": "Issue" -} +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji/344 ``` ## Award Emoji on Notes @@ -222,7 +201,7 @@ Parameters: ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/notes/1/award_emoji ``` Example Response: @@ -264,7 +243,7 @@ Parameters: | `award_id` | integer | yes | The ID of the award emoji | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji/2 +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/notes/1/award_emoji/2 ``` Example Response: @@ -304,7 +283,7 @@ Parameters: | `name` | string | yes | The name of the emoji, without colons | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji?name=rocket +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/notes/1/award_emoji?name=rocket ``` Example Response: @@ -347,28 +326,7 @@ Parameters: | `award_id` | integer | yes | The ID of a award_emoji | ```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/345 -``` - -Example Response: - -```json -{ - "id": 345, - "name": "rocket", - "user": { - "name": "Administrator", - "username": "root", - "id": 1, - "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "web_url": "http://gitlab.example.com/root" - }, - "created_at": "2016-06-17T19:59:55.888Z", - "updated_at": "2016-06-17T19:59:55.888Z", - "awardable_id": 1, - "awardable_type": "Note" -} +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji/345 ``` [ce-4575]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4575 diff --git a/doc/api/boards.md b/doc/api/boards.md index c83db6df80c..a74e82335eb 100644 --- a/doc/api/boards.md +++ b/doc/api/boards.md @@ -18,7 +18,7 @@ GET /projects/:id/boards | `id` | integer | yes | The ID of a project | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/boards +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/boards ``` Example response: @@ -75,7 +75,7 @@ GET /projects/:id/boards/:board_id/lists | `board_id` | integer | yes | The ID of a board | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists ``` Example response: @@ -127,7 +127,7 @@ GET /projects/:id/boards/:board_id/lists/:list_id | `list_id`| integer | yes | The ID of a board's list | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists/1 +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1 ``` Example response: @@ -159,7 +159,7 @@ POST /projects/:id/boards/:board_id/lists | `label_id` | integer | yes | The ID of a label | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists?label_id=5 +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists?label_id=5 ``` Example response: @@ -192,7 +192,7 @@ PUT /projects/:id/boards/:board_id/lists/:list_id | `position` | integer | yes | The position of the list | ```bash -curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists/1?position=2 +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1?position=2 ``` Example response: @@ -224,18 +224,5 @@ DELETE /projects/:id/boards/:board_id/lists/:list_id | `list_id` | integer | yes | The ID of a board's list | ```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists/1 -``` -Example response: - -```json -{ - "id" : 1, - "label" : { - "name" : "Testing", - "color" : "#F0AD4E", - "description" : null - }, - "position" : 1 -} +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1 ``` diff --git a/doc/api/branches.md b/doc/api/branches.md index 765ca439720..83705106160 100644 --- a/doc/api/branches.md +++ b/doc/api/branches.md @@ -13,7 +13,7 @@ GET /projects/:id/repository/branches | `id` | integer | yes | The ID of a project | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/repository/branches ``` Example response: @@ -60,7 +60,7 @@ GET /projects/:id/repository/branches/:branch | `branch` | string | yes | The name of the branch | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/repository/branches/master ``` Example response: @@ -101,7 +101,7 @@ PUT /projects/:id/repository/branches/:branch/protect ``` ```bash -curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master/protect?developers_can_push=true&developers_can_merge=true +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/repository/branches/master/protect?developers_can_push=true&developers_can_merge=true ``` | Attribute | Type | Required | Description | @@ -149,7 +149,7 @@ PUT /projects/:id/repository/branches/:branch/unprotect ``` ```bash -curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master/unprotect +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/repository/branches/master/unprotect ``` | Attribute | Type | Required | Description | @@ -197,7 +197,7 @@ POST /projects/:id/repository/branches | `ref` | string | yes | The branch name or commit SHA to create branch from | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches?branch=newbranch&ref=master" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/branches?branch=newbranch&ref=master" ``` Example response: @@ -241,15 +241,7 @@ DELETE /projects/:id/repository/branches/:branch In case of an error, an explaining message is provided. ```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches/newbranch" -``` - -Example response: - -```json -{ - "branch_name": "newbranch" -} +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/branches/newbranch" ``` ## Delete merged branches @@ -266,5 +258,5 @@ DELETE /projects/:id/repository/merged_branches ```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/merged_branches" +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/merged_branches" ``` diff --git a/doc/api/broadcast_messages.md b/doc/api/broadcast_messages.md index a3e9c01f335..ad254e3515e 100644 --- a/doc/api/broadcast_messages.md +++ b/doc/api/broadcast_messages.md @@ -13,7 +13,7 @@ GET /broadcast_messages ``` ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/broadcast_messages ``` Example response: @@ -43,7 +43,7 @@ GET /broadcast_messages/:id | `id` | integer | yes | Broadcast message ID | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages/1 +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/broadcast_messages/1 ``` Example response: @@ -75,7 +75,7 @@ POST /broadcast_messages | `font` | string | no | Foreground color hex code | ```bash -curl --data "message=Deploy in progress&color=#cecece" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages +curl --data "message=Deploy in progress&color=#cecece" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/broadcast_messages ``` Example response: @@ -108,7 +108,7 @@ PUT /broadcast_messages/:id | `font` | string | no | Foreground color hex code | ```bash -curl --request PUT --data "message=Update message&color=#000" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages/1 +curl --request PUT --data "message=Update message&color=#000" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/broadcast_messages/1 ``` Example response: @@ -136,19 +136,5 @@ DELETE /broadcast_messages/:id | `id` | integer | yes | Broadcast message ID | ```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages/1 -``` - -Example response: - -```json -{ - "message":"Update message", - "starts_at":"2016-08-26T00:41:35.060Z", - "ends_at":"2016-08-26T01:41:35.060Z", - "color":"#000", - "font":"#FFFFFF", - "id":1, - "active": true -} +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/broadcast_messages/1 ``` diff --git a/doc/api/build_triggers.md b/doc/api/build_triggers.md index b6459971420..28befba69d6 100644 --- a/doc/api/build_triggers.md +++ b/doc/api/build_triggers.md @@ -15,7 +15,7 @@ GET /projects/:id/triggers | `id` | integer | yes | The ID of a project | ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers" ``` ```json @@ -51,7 +51,7 @@ GET /projects/:id/triggers/:token | `token` | string | yes | The `token` of a trigger | ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers/7b9148c158980bbd9bcea92c17522d" ``` ```json @@ -77,7 +77,7 @@ POST /projects/:id/triggers | `id` | integer | yes | The ID of a project | ``` -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers" ``` ```json @@ -104,15 +104,5 @@ DELETE /projects/:id/triggers/:token | `token` | string | yes | The `token` of a trigger | ``` -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d" -``` - -```json -{ - "created_at": "2015-12-23T16:25:56.760Z", - "deleted_at": "2015-12-24T12:32:20.100Z", - "last_used": null, - "token": "7b9148c158980bbd9bcea92c17522d", - "updated_at": "2015-12-24T12:32:20.100Z" -} +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers/7b9148c158980bbd9bcea92c17522d" ``` diff --git a/doc/api/build_variables.md b/doc/api/build_variables.md index 917e9773913..1c26e9b33ab 100644 --- a/doc/api/build_variables.md +++ b/doc/api/build_variables.md @@ -13,7 +13,7 @@ GET /projects/:id/variables | `id` | integer | yes | The ID of a project | ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/variables" ``` ```json @@ -43,7 +43,7 @@ GET /projects/:id/variables/:key | `key` | string | yes | The `key` of a variable | ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/TEST_VARIABLE_1" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/variables/TEST_VARIABLE_1" ``` ```json @@ -68,7 +68,7 @@ POST /projects/:id/variables | `value` | string | yes | The `value` of a variable | ``` -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" --form "key=NEW_VARIABLE" --form "value=new value" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/variables" --form "key=NEW_VARIABLE" --form "value=new value" ``` ```json @@ -93,7 +93,7 @@ PUT /projects/:id/variables/:key | `value` | string | yes | The `value` of a variable | ``` -curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/NEW_VARIABLE" --form "value=updated value" +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/variables/NEW_VARIABLE" --form "value=updated value" ``` ```json @@ -117,12 +117,5 @@ DELETE /projects/:id/variables/:key | `key` | string | yes | The `key` of a variable | ``` -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/VARIABLE_1" -``` - -```json -{ - "key": "VARIABLE_1", - "value": "VALUE_1" -} +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/variables/VARIABLE_1" ``` diff --git a/doc/api/builds.md b/doc/api/builds.md index bca2f9e44ef..84214e4708f 100644 --- a/doc/api/builds.md +++ b/doc/api/builds.md @@ -14,7 +14,7 @@ GET /projects/:id/builds | `scope` | string **or** array of strings | no | The scope of builds to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`; showing all builds if none provided | ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v3/projects/1/builds?scope%5B0%5D=pending&scope%5B1%5D=running' +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/1/builds?scope%5B0%5D=pending&scope%5B1%5D=running' ``` Example of response @@ -135,7 +135,7 @@ GET /projects/:id/repository/commits/:sha/builds | `scope` | string **or** array of strings | no | The scope of builds to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`; showing all builds if none provided | ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v3/projects/1/repository/commits/0ff3ae198f8601a285adcf5c0fff204ee6fba5fd/builds?scope%5B0%5D=pending&scope%5B1%5D=running' +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/1/repository/commits/0ff3ae198f8601a285adcf5c0fff204ee6fba5fd/builds?scope%5B0%5D=pending&scope%5B1%5D=running' ``` Example of response @@ -233,7 +233,7 @@ GET /projects/:id/builds/:build_id | `build_id` | integer | yes | The ID of a build | ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/builds/8" ``` Example of response @@ -301,7 +301,7 @@ GET /projects/:id/builds/:build_id/artifacts | `build_id` | integer | yes | The ID of a build | ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8/artifacts" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/builds/8/artifacts" ``` Response: @@ -335,7 +335,7 @@ Parameters Example request: ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/artifacts/master/download?job=test" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/builds/artifacts/master/download?job=test" ``` Example response: @@ -361,7 +361,7 @@ GET /projects/:id/builds/:build_id/trace | build_id | integer | yes | The ID of a build | ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8/trace" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/builds/8/trace" ``` Response: @@ -385,7 +385,7 @@ POST /projects/:id/builds/:build_id/cancel | `build_id` | integer | yes | The ID of a build | ``` -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/cancel" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/builds/1/cancel" ``` Example of response @@ -431,7 +431,7 @@ POST /projects/:id/builds/:build_id/retry | `build_id` | integer | yes | The ID of a build | ``` -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/retry" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/builds/1/retry" ``` Example of response @@ -481,7 +481,7 @@ Parameters Example of request ``` -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/erase" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/builds/1/erase" ``` Example of response @@ -531,7 +531,7 @@ Parameters Example request: ``` -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/artifacts/keep" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/builds/1/artifacts/keep" ``` Example response: @@ -577,7 +577,7 @@ POST /projects/:id/builds/:build_id/play | `build_id` | integer | yes | The ID of a build | ``` -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/play" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/builds/1/play" ``` Example of response diff --git a/doc/api/ci/lint.md b/doc/api/ci/lint.md index 0c96b3ee335..74def207816 100644 --- a/doc/api/ci/lint.md +++ b/doc/api/ci/lint.md @@ -13,7 +13,7 @@ POST ci/lint | `content` | string | yes | the .gitlab-ci.yaml content| ```bash -curl --header "Content-Type: application/json" https://gitlab.example.com/api/v3/ci/lint --data '{"content": "{ \"image\": \"ruby:2.1\", \"services\": [\"postgres\"], \"before_script\": [\"gem install bundler\", \"bundle install\", \"bundle exec rake db:create\"], \"variables\": {\"DB_NAME\": \"postgres\"}, \"types\": [\"test\", \"deploy\", \"notify\"], \"rspec\": { \"script\": \"rake spec\", \"tags\": [\"ruby\", \"postgres\"], \"only\": [\"branches\"]}}"}' +curl --header "Content-Type: application/json" https://gitlab.example.com/api/v4/ci/lint --data '{"content": "{ \"image\": \"ruby:2.1\", \"services\": [\"postgres\"], \"before_script\": [\"gem install bundler\", \"bundle install\", \"bundle exec rake db:create\"], \"variables\": {\"DB_NAME\": \"postgres\"}, \"types\": [\"test\", \"deploy\", \"notify\"], \"rspec\": { \"script\": \"rake spec\", \"tags\": [\"ruby\", \"postgres\"], \"only\": [\"branches\"]}}"}' ``` Be sure to copy paste the exact contents of `.gitlab-ci.yml` as YAML is very picky about indentation and spaces. diff --git a/doc/api/commits.md b/doc/api/commits.md index 18bc2873678..24c402346b1 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -16,7 +16,7 @@ GET /projects/:id/repository/commits | `until` | string | no | Only commits before or on this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/commits" ``` Example response: @@ -114,7 +114,7 @@ PAYLOAD=$(cat << 'JSON' } JSON ) -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data "$PAYLOAD" https://gitlab.example.com/api/v3/projects/1/repository/commits +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data "$PAYLOAD" https://gitlab.example.com/api/v4/projects/1/repository/commits ``` Example response: @@ -159,7 +159,7 @@ Parameters: | `sha` | string | yes | The commit hash or name of a repository branch or tag | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/commits/master ``` Example response: @@ -208,7 +208,7 @@ Parameters: | `branch` | string | yes | The name of the branch | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "branch=master" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/cherry_pick" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "branch=master" "https://gitlab.example.com/api/v4/projects/5/repository/commits/master/cherry_pick" ``` Example response: @@ -249,7 +249,7 @@ Parameters: | `sha` | string | yes | The commit hash or name of a repository branch or tag | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/diff" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/commits/master/diff" ``` Example response: @@ -285,7 +285,7 @@ Parameters: | `sha` | string | yes | The commit hash or name of a repository branch or tag | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/comments" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/commits/master/comments" ``` Example response: @@ -338,7 +338,7 @@ POST /projects/:id/repository/commits/:sha/comments | `line_type` | string | no | The line type. Takes `new` or `old` as arguments | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "note=Nice picture man\!" --form "path=dudeism.md" --form "line=11" --form "line_type=new" https://gitlab.example.com/api/v3/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/comments +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "note=Nice picture man\!" --form "path=dudeism.md" --form "line=11" --form "line_type=new" https://gitlab.example.com/api/v4/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/comments ``` Example response: @@ -383,7 +383,7 @@ GET /projects/:id/repository/commits/:sha/statuses | `all` | boolean | no | Return all statuses, not only the latest ones ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/statuses +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/statuses ``` Example response: @@ -459,7 +459,7 @@ POST /projects/:id/statuses/:sha | `coverage` | float | no | The total code coverage ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/17/statuses/18f3e63d05582537db6d183d9d557be09e1f90c8?state=success" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/17/statuses/18f3e63d05582537db6d183d9d557be09e1f90c8?state=success" ``` Example response: diff --git a/doc/api/deploy_key_multiple_projects.md b/doc/api/deploy_key_multiple_projects.md index 73cb4b7ea8c..f94dbfa4059 100644 --- a/doc/api/deploy_key_multiple_projects.md +++ b/doc/api/deploy_key_multiple_projects.md @@ -7,16 +7,16 @@ First, find the ID of the projects you're interested in, by either listing all projects: ``` -curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/projects +curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v4/projects ``` Or finding the ID of a group and then listing all projects in that group: ``` -curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/groups +curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v4/groups # For group 1234: -curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/groups/1234 +curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v4/groups/1234 ``` With those IDs, add the same deploy key to all: @@ -24,6 +24,6 @@ With those IDs, add the same deploy key to all: ``` for project_id in 321 456 987; do curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" \ - --data '{"title": "my key", "key": "ssh-rsa AAAA..."}' https://gitlab.example.com/api/v3/projects/${project_id}/deploy_keys + --data '{"title": "my key", "key": "ssh-rsa AAAA..."}' https://gitlab.example.com/api/v4/projects/${project_id}/deploy_keys done ``` diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md index 39afc4b2df5..f051f55ac3e 100644 --- a/doc/api/deploy_keys.md +++ b/doc/api/deploy_keys.md @@ -9,7 +9,7 @@ GET /deploy_keys ``` ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/deploy_keys" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/deploy_keys" ``` Example response: @@ -46,7 +46,7 @@ GET /projects/:id/deploy_keys | `id` | integer | yes | The ID of the project | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/deploy_keys" ``` Example response: @@ -86,7 +86,7 @@ Parameters: | `key_id` | integer | yes | The ID of the deploy key | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys/11" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/deploy_keys/11" ``` Example response: @@ -120,7 +120,7 @@ POST /projects/:id/deploy_keys | `can_push` | boolean | no | Can deploy key push to the project's repository | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data '{"title": "My deploy key", "key": "ssh-rsa AAAA...", "can_push": "true"}' "https://gitlab.example.com/api/v3/projects/5/deploy_keys/" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data '{"title": "My deploy key", "key": "ssh-rsa AAAA...", "can_push": "true"}' "https://gitlab.example.com/api/v4/projects/5/deploy_keys/" ``` Example response: @@ -149,19 +149,7 @@ DELETE /projects/:id/deploy_keys/:key_id | `key_id` | integer | yes | The ID of the deploy key | ```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys/13" -``` - -Example response: - -```json -{ - "id": 6, - "deploy_key_id": 14, - "project_id": 1, - "created_at" : "2015-08-29T12:50:57.259Z", - "updated_at" : "2015-08-29T12:50:57.259Z" -} +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/deploy_keys/13" ``` ## Enable a deploy key @@ -169,7 +157,7 @@ Example response: Enables a deploy key for a project so this can be used. Returns the enabled key, with a status code 201 when successful. ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/deploy_keys/13/enable +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/deploy_keys/13/enable ``` | Attribute | Type | Required | Description | diff --git a/doc/api/deployments.md b/doc/api/deployments.md index 3d95c4cde60..76e18c8a9bd 100644 --- a/doc/api/deployments.md +++ b/doc/api/deployments.md @@ -13,7 +13,7 @@ GET /projects/:id/deployments | `id` | integer | yes | The ID of a project | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/deployments" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/deployments" ``` Example of response @@ -151,7 +151,7 @@ GET /projects/:id/deployments/:deployment_id | `deployment_id` | integer | yes | The ID of the deployment | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/deployments/1" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/deployments/1" ``` Example of response diff --git a/doc/api/enviroments.md b/doc/api/enviroments.md index e0ee20d9610..3f0a8d989f9 100644 --- a/doc/api/enviroments.md +++ b/doc/api/enviroments.md @@ -13,7 +13,7 @@ GET /projects/:id/environments | `id` | integer | yes | The ID of the project | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/environments +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/environments ``` Example response: @@ -33,7 +33,7 @@ Example response: Creates a new environment with the given name and external_url. -It returns 201 if the environment was successfully created, 400 for wrong parameters. +It returns `201` if the environment was successfully created, `400` for wrong parameters. ``` POST /projects/:id/environment @@ -46,7 +46,7 @@ POST /projects/:id/environment | `external_url` | string | no | Place to link to for this environment | ```bash -curl --data "name=deploy&external_url=https://deploy.example.gitlab.com" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments" +curl --data "name=deploy&external_url=https://deploy.example.gitlab.com" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/environments" ``` Example response: @@ -64,7 +64,7 @@ Example response: Updates an existing environment's name and/or external_url. -It returns 200 if the environment was successfully updated. In case of an error, a status code 400 is returned. +It returns `200` if the environment was successfully updated. In case of an error, a status code `400` is returned. ``` PUT /projects/:id/environments/:environments_id @@ -78,7 +78,7 @@ PUT /projects/:id/environments/:environments_id | `external_url` | string | no | The new external_url | ```bash -curl --request PUT --data "name=staging&external_url=https://staging.example.gitlab.com" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments/1" +curl --request PUT --data "name=staging&external_url=https://staging.example.gitlab.com" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/environments/1" ``` Example response: @@ -94,7 +94,7 @@ Example response: ## Delete an environment -It returns 200 if the environment was successfully deleted, and 404 if the environment does not exist. +It returns `200` if the environment was successfully deleted, and `404` if the environment does not exist. ``` DELETE /projects/:id/environments/:environment_id @@ -106,7 +106,24 @@ DELETE /projects/:id/environments/:environment_id | `environment_id` | integer | yes | The ID of the environment | ```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments/1" +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/environments/1" +``` + +## Stop an environment + +It returns `200` if the environment was successfully stopped, and `404` if the environment does not exist. + +``` +POST /projects/:id/environments/:environment_id/stop +``` + +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | --------------------- | +| `id` | integer | yes | The ID of the project | +| `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" ``` Example response: diff --git a/doc/api/groups.md b/doc/api/groups.md index 39adb5be502..80c08096dea 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -27,7 +27,7 @@ GET /groups "name": "Foobar Group", "path": "foo-bar", "description": "An interesting group", - "visibility_level": 20, + "visibility": "public", "lfs_enabled": true, "avatar_url": "http://localhost:3000/uploads/group/avatar/1/foo.jpg", "web_url": "http://localhost:3000/groups/foo-bar", @@ -72,9 +72,8 @@ Example response: "description": "foo", "default_branch": "master", "tag_list": [], - "public": false, "archived": false, - "visibility_level": 10, + "visibility": "internal", "ssh_url_to_repo": "git@gitlab.example.com/html5-boilerplate.git", "http_url_to_repo": "http://gitlab.example.com/h5bp/html5-boilerplate.git", "web_url": "http://gitlab.example.com/h5bp/html5-boilerplate", @@ -123,7 +122,7 @@ Parameters: | `id` | integer/string | yes | The ID or path of a group | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/4 +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/4 ``` Example response: @@ -134,7 +133,7 @@ Example response: "name": "Twitter", "path": "twitter", "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.", - "visibility_level": 20, + "visibility": "public", "avatar_url": null, "web_url": "https://gitlab.example.com/groups/twitter", "request_access_enabled": false, @@ -147,9 +146,8 @@ Example response: "description": "Voluptas veniam qui et beatae voluptas doloremque explicabo facilis.", "default_branch": "master", "tag_list": [], - "public": true, "archived": false, - "visibility_level": 20, + "visibility": "public", "ssh_url_to_repo": "git@gitlab.example.com:twitter/typeahead-js.git", "http_url_to_repo": "https://gitlab.example.com/twitter/typeahead-js.git", "web_url": "https://gitlab.example.com/twitter/typeahead-js", @@ -186,9 +184,8 @@ Example response: "description": "Aspernatur omnis repudiandae qui voluptatibus eaque.", "default_branch": "master", "tag_list": [], - "public": false, "archived": false, - "visibility_level": 10, + "visibility": "internal", "ssh_url_to_repo": "git@gitlab.example.com:twitter/flight.git", "http_url_to_repo": "https://gitlab.example.com/twitter/flight.git", "web_url": "https://gitlab.example.com/twitter/flight", @@ -227,9 +224,8 @@ Example response: "description": "Velit eveniet provident fugiat saepe eligendi autem.", "default_branch": "master", "tag_list": [], - "public": false, "archived": false, - "visibility_level": 0, + "visibility": "private", "ssh_url_to_repo": "git@gitlab.example.com:h5bp/html5-boilerplate.git", "http_url_to_repo": "https://gitlab.example.com/h5bp/html5-boilerplate.git", "web_url": "https://gitlab.example.com/h5bp/html5-boilerplate", @@ -288,7 +284,7 @@ Parameters: - `name` (required) - The name of the group - `path` (required) - The path of the group - `description` (optional) - The group's description -- `visibility_level` (optional) - The group's visibility. 0 for private, 10 for internal, 20 for public. +- `visibility` (optional) - The group's visibility. Can be `private`, `internal`, or `public`. - `lfs_enabled` (optional) - Enable/disable Large File Storage (LFS) for the projects in this group - `request_access_enabled` (optional) - Allow users to request member access. - `parent_id` (optional) - The parent group id for creating nested group. @@ -320,12 +316,12 @@ PUT /groups/:id | `name` | string | no | The name of the group | | `path` | string | no | The path of the group | | `description` | string | no | The description of the group | -| `visibility_level` | integer | no | The visibility level of the group. 0 for private, 10 for internal, 20 for public. | +| `visibility` | string | no | The visibility level of the group. Can be `private`, `internal`, or `public`. | | `lfs_enabled` (optional) | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group | | `request_access_enabled` | boolean | no | Allow users to request member access. | ```bash -curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/groups/5?name=Experimental" +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/groups/5?name=Experimental" ``` @@ -337,7 +333,7 @@ Example response: "name": "Experimental", "path": "h5bp", "description": "foo", - "visibility_level": 10, + "visibility": "internal", "avatar_url": null, "web_url": "http://gitlab.example.com/groups/h5bp", "request_access_enabled": false, @@ -352,7 +348,7 @@ Example response: "tag_list": [], "public": false, "archived": false, - "visibility_level": 10, + "visibility": "internal", "ssh_url_to_repo": "git@gitlab.example.com/html5-boilerplate.git", "http_url_to_repo": "http://gitlab.example.com/h5bp/html5-boilerplate.git", "web_url": "http://gitlab.example.com/h5bp/html5-boilerplate", diff --git a/doc/api/issues.md b/doc/api/issues.md index 5266077e098..51ce08c873e 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -25,6 +25,7 @@ GET /issues?labels=foo,bar GET /issues?labels=foo,bar&state=opened GET /issues?milestone=1.0.0 GET /issues?milestone=1.0.0&state=opened +GET /issues?iids[]=42&iids[]=43 ``` | Attribute | Type | Required | Description | @@ -32,11 +33,12 @@ GET /issues?milestone=1.0.0&state=opened | `state` | string | no | Return all issues or just those that are `opened` or `closed`| | `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned | | `milestone` | string| no | The milestone title | +| `iids` | Array[integer] | no | Return only the issues having the given `iid` | | `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/issues +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/issues ``` Example response: @@ -102,6 +104,7 @@ GET /groups/:id/issues?labels=foo,bar GET /groups/:id/issues?labels=foo,bar&state=opened GET /groups/:id/issues?milestone=1.0.0 GET /groups/:id/issues?milestone=1.0.0&state=opened +GET /groups/:id/issues?iids[]=42&iids[]=43 ``` | Attribute | Type | Required | Description | @@ -109,13 +112,14 @@ GET /groups/:id/issues?milestone=1.0.0&state=opened | `id` | integer | yes | The ID of a group | | `state` | string | no | Return all issues or just those that are `opened` or `closed`| | `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned | +| `iids` | Array[integer] | no | Return only the issues having the given `iid` | | `milestone` | string| no | The milestone title | | `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/4/issues +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/4/issues ``` Example response: @@ -181,12 +185,13 @@ GET /projects/:id/issues?labels=foo,bar GET /projects/:id/issues?labels=foo,bar&state=opened GET /projects/:id/issues?milestone=1.0.0 GET /projects/:id/issues?milestone=1.0.0&state=opened +GET /projects/:id/issues?iids[]=42&iids[]=43 ``` | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer | yes | The ID of a project | -| `iid` | integer | no | Return the issue having the given `iid` | +| `iids` | Array[integer] | no | Return only the milestone having the given `iid` | | `state` | string | no | Return all issues or just those that are `opened` or `closed`| | `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned | | `milestone` | string| no | The milestone title | @@ -195,7 +200,7 @@ GET /projects/:id/issues?milestone=1.0.0&state=opened ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues ``` Example response: @@ -262,7 +267,7 @@ GET /projects/:id/issues/:issue_id | `issue_id`| integer | yes | The ID of a project's issue | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/41 +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues/41 ``` Example response: @@ -335,7 +340,7 @@ POST /projects/:id/issues | `merge_request_for_resolving_discussions` | integer | no | The IID of a merge request in which to resolve all issues. This will fill the issue with a default description and mark all discussions as resolved. When passing a description or title, these values will take precedence over the default values. | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues?title=Issues%20with%20auth&labels=bug +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues?title=Issues%20with%20auth&labels=bug ``` Example response: @@ -395,7 +400,7 @@ PUT /projects/:id/issues/:issue_id | `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` | ```bash -curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85?state_event=close +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues/85?state_event=close ``` Example response: @@ -445,7 +450,7 @@ DELETE /projects/:id/issues/:issue_id | `issue_id` | integer | yes | The ID of a project's issue | ```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85 +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues/85 ``` ## Move an issue @@ -468,7 +473,7 @@ POST /projects/:id/issues/:issue_id/move | `to_project_id` | integer | yes | The ID of the new project | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85/move +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues/85/move ``` Example response: @@ -523,7 +528,7 @@ POST /projects/:id/issues/:issue_id/subscribe | `issue_id` | integer | yes | The ID of a project's issue | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscribe +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/subscribe ``` Example response: @@ -578,44 +583,7 @@ POST /projects/:id/issues/:issue_id/unsubscribe | `issue_id` | integer | yes | The ID of a project's issue | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/unsubscribe -``` - -Example response: - -```json -{ - "id": 93, - "iid": 12, - "project_id": 5, - "title": "Incidunt et rerum ea expedita iure quibusdam.", - "description": "Et cumque architecto sed aut ipsam.", - "state": "opened", - "created_at": "2016-04-05T21:41:45.217Z", - "updated_at": "2016-04-07T13:02:37.905Z", - "labels": [], - "milestone": null, - "assignee": { - "name": "Edwardo Grady", - "username": "keyon", - "id": 21, - "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/3e6f06a86cf27fa8b56f3f74f7615987?s=80&d=identicon", - "web_url": "https://gitlab.example.com/keyon" - }, - "author": { - "name": "Vivian Hermann", - "username": "orville", - "id": 11, - "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", - "web_url": "https://gitlab.example.com/orville" - }, - "subscribed": false, - "due_date": null, - "web_url": "http://example.com/example/example/issues/12", - "confidential": false -} +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/unsubscribe ``` ## Create a todo @@ -634,7 +602,7 @@ POST /projects/:id/issues/:issue_id/todo | `issue_id` | integer | yes | The ID of a project's issue | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/todo +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/todo ``` Example response: @@ -726,7 +694,7 @@ POST /projects/:id/issues/:issue_id/time_estimate | `duration` | string | yes | The duration in human format. e.g: 3h30m | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/time_estimate?duration=3h30m +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/time_estimate?duration=3h30m ``` Example response: @@ -754,7 +722,7 @@ POST /projects/:id/issues/:issue_id/reset_time_estimate | `issue_id` | integer | yes | The ID of a project's issue | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/reset_time_estimate +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/reset_time_estimate ``` Example response: @@ -783,7 +751,7 @@ POST /projects/:id/issues/:issue_id/add_spent_time | `duration` | string | yes | The duration in human format. e.g: 3h30m | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/add_spent_time?duration=1h +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/add_spent_time?duration=1h ``` Example response: @@ -811,7 +779,7 @@ POST /projects/:id/issues/:issue_id/reset_spent_time | `issue_id` | integer | yes | The ID of a project's issue | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/reset_spent_time +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/reset_spent_time ``` Example response: @@ -837,7 +805,7 @@ GET /projects/:id/issues/:issue_id/time_stats | `issue_id` | integer | yes | The ID of a project's issue | ```bash -curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/time_stats +curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/time_stats ``` Example response: diff --git a/doc/api/labels.md b/doc/api/labels.md index 8e0855fe9e2..e8c220f6809 100644 --- a/doc/api/labels.md +++ b/doc/api/labels.md @@ -13,7 +13,7 @@ GET /projects/:id/labels | `id` | integer | yes | The ID of the project | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/labels +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/labels ``` Example response: @@ -95,7 +95,7 @@ POST /projects/:id/labels | `priority` | integer | no | The priority of the label. Must be greater or equal than zero or `null` to remove the priority. | ```bash -curl --data "name=feature&color=#5843AD" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels" +curl --data "name=feature&color=#5843AD" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/labels" ``` Example response: @@ -128,23 +128,7 @@ DELETE /projects/:id/labels | `name` | string | yes | The name of the label | ```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels?name=bug" -``` - -Example response: - -```json -{ - "id" : 1, - "name" : "bug", - "color" : "#d9534f", - "description": "Bug reported by user", - "open_issues_count": 1, - "closed_issues_count": 0, - "open_merge_requests_count": 1, - "subscribed": false, - "priority": null -} +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/labels?name=bug" ``` ## Edit an existing label @@ -167,7 +151,7 @@ PUT /projects/:id/labels ```bash -curl --request PUT --data "name=documentation&new_name=docs&color=#8E44AD&description=Documentation" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels" +curl --request PUT --data "name=documentation&new_name=docs&color=#8E44AD&description=Documentation" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/labels" ``` Example response: @@ -202,7 +186,7 @@ POST /projects/:id/labels/:label_id/subscribe | `label_id` | integer or string | yes | The ID or title of a project's label | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscribe +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/labels/1/subscribe ``` Example response: @@ -237,21 +221,5 @@ POST /projects/:id/labels/:label_id/unsubscribe | `label_id` | integer or string | yes | The ID or title of a project's label | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/unsubscribe -``` - -Example response: - -```json -{ - "id" : 1, - "name" : "bug", - "color" : "#d9534f", - "description": "Bug reported by user", - "open_issues_count": 1, - "closed_issues_count": 0, - "open_merge_requests_count": 1, - "subscribed": false, - "priority": null -} +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/labels/1/unsubscribe ``` diff --git a/doc/api/members.md b/doc/api/members.md index 5dcb2a5f60a..fe46f8f84bc 100644 --- a/doc/api/members.md +++ b/doc/api/members.md @@ -27,8 +27,8 @@ GET /projects/:id/members | `query` | string | no | A query string to search for members | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/members +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/members ``` Example response: @@ -69,8 +69,8 @@ GET /projects/:id/members/:user_id | `user_id` | integer | yes | The user ID of the member | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/members/:user_id +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/members/:user_id ``` Example response: @@ -104,8 +104,8 @@ POST /projects/:id/members | `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "user_id=1&access_level=30" https://gitlab.example.com/api/v3/groups/:id/members -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "user_id=1&access_level=30" https://gitlab.example.com/api/v3/projects/:id/members +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "user_id=1&access_level=30" https://gitlab.example.com/api/v4/groups/:id/members +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "user_id=1&access_level=30" https://gitlab.example.com/api/v4/projects/:id/members ``` Example response: @@ -138,8 +138,8 @@ PUT /projects/:id/members/:user_id | `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY | ```bash -curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id?access_level=40 -curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id?access_level=40 +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/members/:user_id?access_level=40 +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/members/:user_id?access_level=40 ``` Example response: @@ -170,6 +170,6 @@ DELETE /projects/:id/members/:user_id | `user_id` | integer | yes | The user ID of the member | ```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/members/:user_id +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/members/:user_id ``` diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index ea30a163a12..e178d5c1629 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -65,7 +65,7 @@ Parameters: "updated_at": "2015-02-02T19:49:26.013Z", "due_date": null }, - "merge_when_build_succeeds": true, + "merge_when_pipeline_succeeds": true, "merge_status": "can_be_merged", "subscribed" : false, "sha": "8888888888888888888888888888888888888888", @@ -134,7 +134,7 @@ Parameters: "updated_at": "2015-02-02T19:49:26.013Z", "due_date": null }, - "merge_when_build_succeeds": true, + "merge_when_pipeline_succeeds": true, "merge_status": "can_be_merged", "subscribed" : true, "sha": "8888888888888888888888888888888888888888", @@ -239,7 +239,7 @@ Parameters: "updated_at": "2015-02-02T19:49:26.013Z", "due_date": null }, - "merge_when_build_succeeds": true, + "merge_when_pipeline_succeeds": true, "merge_status": "can_be_merged", "subscribed" : true, "sha": "8888888888888888888888888888888888888888", @@ -326,7 +326,7 @@ POST /projects/:id/merge_requests "updated_at": "2015-02-02T19:49:26.013Z", "due_date": null }, - "merge_when_build_succeeds": true, + "merge_when_pipeline_succeeds": true, "merge_status": "can_be_merged", "subscribed" : true, "sha": "8888888888888888888888888888888888888888", @@ -403,7 +403,7 @@ Must include at least one non-required attribute from above. "updated_at": "2015-02-02T19:49:26.013Z", "due_date": null }, - "merge_when_build_succeeds": true, + "merge_when_pipeline_succeeds": true, "merge_status": "can_be_merged", "subscribed" : true, "sha": "8888888888888888888888888888888888888888", @@ -429,7 +429,7 @@ DELETE /projects/:id/merge_requests/:merge_request_id | `merge_request_id` | integer | yes | The ID of a project's merge request | ```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/merge_requests/85 +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/merge_requests/85 ``` ## Accept MR @@ -455,7 +455,7 @@ Parameters: - `merge_request_id` (required) - ID of MR - `merge_commit_message` (optional) - Custom merge commit message - `should_remove_source_branch` (optional) - if `true` removes the source branch -- `merge_when_build_succeeds` (optional) - if `true` the MR is merged when the build succeeds +- `merge_when_pipeline_succeeds` (optional) - if `true` the MR is merged when the pipeline succeeds - `sha` (optional) - if present, then this SHA must match the HEAD of the source branch, otherwise the merge will fail ```json @@ -501,7 +501,7 @@ Parameters: "updated_at": "2015-02-02T19:49:26.013Z", "due_date": null }, - "merge_when_build_succeeds": true, + "merge_when_pipeline_succeeds": true, "merge_status": "can_be_merged", "subscribed" : true, "sha": "8888888888888888888888888888888888888888", @@ -519,9 +519,9 @@ If you don't have permissions to accept this merge request - you'll get a `401` If the merge request is already merged or closed - you get `405` and error message 'Method Not Allowed' -In case the merge request is not set to be merged when the build succeeds, you'll also get a `406` error. +In case the merge request is not set to be merged when the pipeline succeeds, you'll also get a `406` error. ``` -PUT /projects/:id/merge_requests/:merge_request_id/cancel_merge_when_build_succeeds +PUT /projects/:id/merge_requests/:merge_request_id/cancel_merge_when_pipeline_succeeds ``` Parameters: @@ -571,7 +571,7 @@ Parameters: "updated_at": "2015-02-02T19:49:26.013Z", "due_date": null }, - "merge_when_build_succeeds": true, + "merge_when_pipeline_succeeds": true, "merge_status": "can_be_merged", "subscribed" : true, "sha": "8888888888888888888888888888888888888888", @@ -601,7 +601,7 @@ GET /projects/:id/merge_requests/:merge_request_id/closes_issues | `merge_request_id` | integer | yes | The ID of the merge request | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/76/merge_requests/1/closes_issues +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/76/merge_requests/1/closes_issues ``` Example response when the GitLab issue tracker is used: @@ -676,7 +676,7 @@ POST /projects/:id/merge_requests/:merge_request_id/subscribe | `merge_request_id` | integer | yes | The ID of the merge request | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscribe +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/17/subscribe ``` Example response: @@ -726,7 +726,7 @@ Example response: "updated_at": "2016-04-05T21:41:40.905Z", "due_date": null }, - "merge_when_build_succeeds": false, + "merge_when_pipeline_succeeds": false, "merge_status": "cannot_be_merged", "subscribed": true, "sha": "8888888888888888888888888888888888888888", @@ -750,7 +750,7 @@ POST /projects/:id/merge_requests/:merge_request_id/unsubscribe | `merge_request_id` | integer | yes | The ID of the merge request | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/unsubscribe +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/17/unsubscribe ``` Example response: @@ -800,7 +800,7 @@ Example response: "updated_at": "2016-04-05T21:41:40.905Z", "due_date": null }, - "merge_when_build_succeeds": false, + "merge_when_pipeline_succeeds": false, "merge_status": "cannot_be_merged", "subscribed": false, "sha": "8888888888888888888888888888888888888888", @@ -824,7 +824,7 @@ POST /projects/:id/merge_requests/:merge_request_id/todo | `merge_request_id` | integer | yes | The ID of the merge request | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/27/todo +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/27/todo ``` Example response: @@ -893,7 +893,7 @@ Example response: "updated_at": "2016-06-17T07:47:33.840Z", "due_date": null }, - "merge_when_build_succeeds": false, + "merge_when_pipeline_succeeds": false, "merge_status": "unchecked", "subscribed": true, "sha": "8888888888888888888888888888888888888888", @@ -924,7 +924,7 @@ GET /projects/:id/merge_requests/:merge_request_id/versions | `merge_request_id` | integer | yes | The ID of the merge request | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/merge_requests/1/versions +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/merge_requests/1/versions ``` Example response: @@ -966,7 +966,7 @@ GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id | `version_id` | integer | yes | The ID of the merge request diff version | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/merge_requests/1/versions/1 +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/merge_requests/1/versions/1 ``` Example response: @@ -1033,7 +1033,7 @@ POST /projects/:id/merge_requests/:merge_request_id/time_estimate | `duration` | string | yes | The duration in human format. e.g: 3h30m | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/time_estimate?duration=3h30m +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/93/time_estimate?duration=3h30m ``` Example response: @@ -1061,7 +1061,7 @@ POST /projects/:id/merge_requests/:merge_request_id/reset_time_estimate | `merge_request_id` | integer | yes | The ID of a project's merge_request | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/reset_time_estimate +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/93/reset_time_estimate ``` Example response: @@ -1090,7 +1090,7 @@ POST /projects/:id/merge_requests/:merge_request_id/add_spent_time | `duration` | string | yes | The duration in human format. e.g: 3h30m | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/add_spent_time?duration=1h +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/93/add_spent_time?duration=1h ``` Example response: @@ -1118,7 +1118,7 @@ POST /projects/:id/merge_requests/:merge_request_id/reset_spent_time | `merge_request_id` | integer | yes | The ID of a project's merge_request | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/reset_spent_time +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/93/reset_spent_time ``` Example response: @@ -1144,7 +1144,7 @@ GET /projects/:id/merge_requests/:merge_request_id/time_stats | `merge_request_id` | integer | yes | The ID of a project's merge request | ```bash -curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/time_stats +curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/93/time_stats ``` Example response: diff --git a/doc/api/milestones.md b/doc/api/milestones.md index bf7dcc008e9..3c86357a6c3 100644 --- a/doc/api/milestones.md +++ b/doc/api/milestones.md @@ -6,10 +6,11 @@ Returns a list of project milestones. ``` GET /projects/:id/milestones -GET /projects/:id/milestones?iid=42 -GET /projects/:id/milestones?iid[]=42&iid[]=43 +GET /projects/:id/milestones?iids=42 +GET /projects/:id/milestones?iids[]=42&iids[]=43 GET /projects/:id/milestones?state=active GET /projects/:id/milestones?state=closed +GET /projects/:id/milestones?search=version ``` Parameters: @@ -17,11 +18,12 @@ Parameters: | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer | yes | The ID of a project | -| `iid` | Array[integer] | optional | Return only the milestone having the given `iid` | -| `state` | string | optional | Return only `active` or `closed` milestones` | +| `iids` | Array[integer] | optional | Return only the milestones having the given `iids` | +| `state` | string | optional | Return only `active` or `closed` milestones` | +| `search` | string | optional | Return only milestones with a title or description matching the provided string | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/milestones +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/milestones ``` Example Response: @@ -115,4 +117,4 @@ GET /projects/:id/milestones/:milestone_id/merge_requests Parameters: - `id` (required) - The ID of a project -- `milestone_id` (required) - The ID of a project milestone
\ No newline at end of file +- `milestone_id` (required) - The ID of a project milestone diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md index 1d97b5de688..eef06d5f324 100644 --- a/doc/api/namespaces.md +++ b/doc/api/namespaces.md @@ -19,7 +19,7 @@ GET /namespaces Example request: ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/namespaces +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/namespaces ``` Example response: @@ -60,7 +60,7 @@ GET /namespaces?search=foobar Example request: ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/namespaces?search=twitter +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/namespaces?search=twitter ``` Example response: diff --git a/doc/api/notes.md b/doc/api/notes.md index dced821cc6d..6ef06b2c2e9 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -120,31 +120,7 @@ Parameters: | `note_id` | integer | yes | The ID of a note | ```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/11/notes/636 -``` - -Example Response: - -```json -{ - "id": 636, - "body": "This is a good idea.", - "attachment": null, - "author": { - "id": 1, - "username": "pipin", - "email": "admin@example.com", - "name": "Pip", - "state": "active", - "created_at": "2013-09-30T13:46:01Z", - "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", - "web_url": "https://gitlab.example.com/pipin" - }, - "created_at": "2016-04-05T22:10:44.164Z", - "system": false, - "noteable_id": 11, - "noteable_type": "Issue" -} +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/11/notes/636 ``` ## Snippets @@ -242,31 +218,7 @@ Parameters: | `note_id` | integer | yes | The ID of a note | ```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/snippets/52/notes/1659 -``` - -Example Response: - -```json -{ - "id": 1659, - "body": "This is a good idea.", - "attachment": null, - "author": { - "id": 1, - "username": "pipin", - "email": "admin@example.com", - "name": "Pip", - "state": "active", - "created_at": "2013-09-30T13:46:01Z", - "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", - "web_url": "https://gitlab.example.com/pipin" - }, - "created_at": "2016-04-06T16:51:53.239Z", - "system": false, - "noteable_id": 52, - "noteable_type": "Snippet" -} +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/snippets/52/notes/1659 ``` ## Merge Requests @@ -367,29 +319,5 @@ Parameters: | `note_id` | integer | yes | The ID of a note | ```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/7/notes/1602 -``` - -Example Response: - -```json -{ - "id": 1602, - "body": "This is a good idea.", - "attachment": null, - "author": { - "id": 1, - "username": "pipin", - "email": "admin@example.com", - "name": "Pip", - "state": "active", - "created_at": "2013-09-30T13:46:01Z", - "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", - "web_url": "https://gitlab.example.com/pipin" - }, - "created_at": "2016-04-05T22:11:59.923Z", - "system": false, - "noteable_id": 7, - "noteable_type": "MergeRequest" -} +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/7/notes/1602 ``` diff --git a/doc/api/notification_settings.md b/doc/api/notification_settings.md index aea1c12a392..43047917f77 100644 --- a/doc/api/notification_settings.md +++ b/doc/api/notification_settings.md @@ -41,7 +41,7 @@ GET /notification_settings ``` ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/notification_settings +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/notification_settings ``` Example response: @@ -62,7 +62,7 @@ PUT /notification_settings ``` ```bash -curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/notification_settings?level=watch +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/notification_settings?level=watch ``` | Attribute | Type | Required | Description | @@ -101,8 +101,8 @@ GET /projects/:id/notification_settings ``` ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/5/notification_settings -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/8/notification_settings +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/5/notification_settings +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/8/notification_settings ``` | Attribute | Type | Required | Description | @@ -127,8 +127,8 @@ PUT /projects/:id/notification_settings ``` ```bash -curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/5/notification_settings?level=watch -curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/8/notification_settings?level=custom&new_note=true +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/5/notification_settings?level=watch +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/8/notification_settings?level=custom&new_note=true ``` | Attribute | Type | Required | Description | diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md index eab532af594..46fe64d382e 100644 --- a/doc/api/oauth2.md +++ b/doc/api/oauth2.md @@ -77,13 +77,13 @@ You can now make requests to the API with the access token returned. The access token allows you to make requests to the API on a behalf of a user. ``` -GET https://gitlab.example.com/api/v3/user?access_token=OAUTH-TOKEN +GET https://gitlab.example.com/api/v4/user?access_token=OAUTH-TOKEN ``` Or you can put the token to the Authorization header: ``` -curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v3/user +curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v4/user ``` ## Resource Owner Password Credentials diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md index f3c9827f742..9d6f3ea41d9 100644 --- a/doc/api/pipelines.md +++ b/doc/api/pipelines.md @@ -13,7 +13,7 @@ GET /projects/:id/pipelines | `id` | integer | yes | The ID of a project | ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipelines" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipelines" ``` Example of response @@ -85,7 +85,7 @@ GET /projects/:id/pipelines/:pipeline_id | `pipeline_id` | integer | yes | The ID of a pipeline | ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipeline/46" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipeline/46" ``` Example of response @@ -131,7 +131,7 @@ POST /projects/:id/pipeline | `ref` | string | yes | Reference to commit | ``` -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipeline?ref=master" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipeline?ref=master" ``` Example of response @@ -177,7 +177,7 @@ POST /projects/:id/pipelines/:pipeline_id/retry | `pipeline_id` | integer | yes | The ID of a pipeline | ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipelines/46/retry" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipelines/46/retry" ``` Response: @@ -223,7 +223,7 @@ POST /projects/:id/pipelines/:pipeline_id/cancel | `pipeline_id` | integer | yes | The ID of a pipeline | ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipelines/46/cancel" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipelines/46/cancel" ``` Response: diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md index 404876f6237..4f6f561b83e 100644 --- a/doc/api/project_snippets.md +++ b/doc/api/project_snippets.md @@ -3,15 +3,15 @@ ### Snippet visibility level Snippets in GitLab can be either private, internal or public. -You can set it with the `visibility_level` field in the snippet. +You can set it with the `visibility` field in the snippet. Constants for snippet visibility levels are: -| Visibility | visibility_level | Description | -| ---------- | ---------------- | ----------- | -| Private | `0` | The snippet is visible only the snippet creator | -| Internal | `10` | The snippet is visible for any logged in user | -| Public | `20` | The snippet can be accessed without any authentication | +| visibility | Description | +| ---------- | ----------- | +| `private` | The snippet is visible only the snippet creator | +| `internal` | The snippet is visible for any logged in user | +| `public` | The snippet can be accessed without any authentication | ## List snippets @@ -71,7 +71,7 @@ Parameters: - `title` (required) - The title of a snippet - `file_name` (required) - The name of a snippet file - `code` (required) - The content of a snippet -- `visibility_level` (required) - The snippet's visibility +- `visibility` (required) - The snippet's visibility ## Update snippet @@ -88,7 +88,7 @@ Parameters: - `title` (optional) - The title of a snippet - `file_name` (optional) - The name of a snippet file - `code` (optional) - The content of a snippet -- `visibility_level` (optional) - The snippet's visibility +- `visibility` (optional) - The snippet's visibility ## Delete snippet diff --git a/doc/api/projects.md b/doc/api/projects.md index 1a8c0ae758f..6062c5ccd71 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -4,17 +4,17 @@ ### Project visibility level Project in GitLab has be either private, internal or public. -You can determine it by `visibility_level` field in project. +You can determine it by `visibility` field in project. Constants for project visibility levels are next: -* Private. `visibility_level` is `0`. +* `private`: Project access must be granted explicitly for each user. -* Internal. `visibility_level` is `10`. +* `internal`: The project can be cloned by any logged in user. -* Public. `visibility_level` is `20`. +* `public`: The project can be cloned without any authentication. @@ -34,9 +34,10 @@ Parameters: | `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` | | `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` | | `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` | -| `search` | string | no | Return list of authorized projects matching the search criteria | +| `search` | string | no | Return list of projects matching the search criteria | | `simple` | boolean | no | Return only the ID, URL, name, and path of each project | | `owned` | boolean | no | Limit by projects owned by the current user | +| `membership` | boolean | no | Limit by projects that the current user is a member of | | `starred` | boolean | no | Limit by projects starred by the current user | ```json @@ -45,8 +46,7 @@ Parameters: "id": 4, "description": null, "default_branch": "master", - "public": false, - "visibility_level": 0, + "visibility": "private", "ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git", "http_url_to_repo": "http://example.com/diaspora/diaspora-client.git", "web_url": "http://example.com/diaspora/diaspora-client", @@ -88,7 +88,7 @@ Parameters: "runners_token": "b8547b1dc37721d05889db52fa2f02", "public_builds": true, "shared_with_groups": [], - "only_allow_merge_if_build_succeeds": false, + "only_allow_merge_if_pipeline_succeeds": false, "only_allow_merge_if_all_discussions_are_resolved": false, "request_access_enabled": false }, @@ -96,8 +96,7 @@ Parameters: "id": 6, "description": null, "default_branch": "master", - "public": false, - "visibility_level": 0, + "visibility": "private", "ssh_url_to_repo": "git@example.com:brightbox/puppet.git", "http_url_to_repo": "http://example.com/brightbox/puppet.git", "web_url": "http://example.com/brightbox/puppet", @@ -149,7 +148,7 @@ Parameters: "runners_token": "b8547b1dc37721d05889db52fa2f02", "public_builds": true, "shared_with_groups": [], - "only_allow_merge_if_build_succeeds": false, + "only_allow_merge_if_pipeline_succeeds": false, "only_allow_merge_if_all_discussions_are_resolved": false, "request_access_enabled": false } @@ -177,8 +176,7 @@ Parameters: "id": 3, "description": null, "default_branch": "master", - "public": false, - "visibility_level": 0, + "visibility": "private", "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git", "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", "web_url": "http://example.com/diaspora/diaspora-project-site", @@ -241,7 +239,7 @@ Parameters: "group_access_level": 10 } ], - "only_allow_merge_if_build_succeeds": false, + "only_allow_merge_if_pipeline_succeeds": false, "only_allow_merge_if_all_discussions_are_resolved": false, "request_access_enabled": false } @@ -435,8 +433,8 @@ Parameters: | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `name` | string | yes | The name of the new project | -| `path` | string | no | Custom repository name for new project. By default generated based on name | +| `name` | string | yes if path is not provided | The name of the new project. Equals path if not provided. | +| `path` | string | yes if name is not provided | Repository name for new project. Generated based on name if not provided (generated lowercased with dashes). | | `namespace_id` | integer | no | Namespace for the new project (defaults to the current user's namespace) | | `description` | string | no | Short project description | | `issues_enabled` | boolean | no | Enable issues for this project | @@ -446,10 +444,10 @@ Parameters: | `snippets_enabled` | boolean | no | Enable snippets for this project | | `container_registry_enabled` | boolean | no | Enable container registry for this project | | `shared_runners_enabled` | boolean | no | Enable shared runners for this project | -| `visibility_level` | integer | no | See [project visibility level](#project-visibility-level) | +| `visibility` | String | no | See [project visibility level](#project-visibility-level) | | `import_url` | string | no | URL to import repository from | | `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members | -| `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds | +| `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds | | `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved | | `lfs_enabled` | boolean | no | Enable LFS | | `request_access_enabled` | boolean | no | Allow users to request member access | @@ -479,10 +477,10 @@ Parameters: | `snippets_enabled` | boolean | no | Enable snippets for this project | | `container_registry_enabled` | boolean | no | Enable container registry for this project | | `shared_runners_enabled` | boolean | no | Enable shared runners for this project | -| `visibility_level` | integer | no | See [project visibility level](#project-visibility-level) | +| `visibility` | string | no | See [project visibility level](#project-visibility-level) | | `import_url` | string | no | URL to import repository from | | `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members | -| `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds | +| `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds | | `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved | | `lfs_enabled` | boolean | no | Enable LFS | | `request_access_enabled` | boolean | no | Allow users to request member access | @@ -511,10 +509,10 @@ Parameters: | `snippets_enabled` | boolean | no | Enable snippets for this project | | `container_registry_enabled` | boolean | no | Enable container registry for this project | | `shared_runners_enabled` | boolean | no | Enable shared runners for this project | -| `visibility_level` | integer | no | See [project visibility level](#project-visibility-level) | +| `visibility` | string | no | See [project visibility level](#project-visibility-level) | | `import_url` | string | no | URL to import repository from | | `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members | -| `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds | +| `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds | | `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved | | `lfs_enabled` | boolean | no | Enable LFS | | `request_access_enabled` | boolean | no | Allow users to request member access | @@ -549,7 +547,7 @@ Parameters: | `id` | integer/string | yes | The ID or NAMESPACE/PROJECT_NAME of the project | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/star" ``` Example response: @@ -559,8 +557,7 @@ Example response: "id": 3, "description": null, "default_branch": "master", - "public": false, - "visibility_level": 10, + "visibility": "internal", "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git", "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", "web_url": "http://example.com/diaspora/diaspora-project-site", @@ -596,7 +593,7 @@ Example response: "star_count": 1, "public_builds": true, "shared_with_groups": [], - "only_allow_merge_if_build_succeeds": false, + "only_allow_merge_if_pipeline_succeeds": false, "only_allow_merge_if_all_discussions_are_resolved": false, "request_access_enabled": false } @@ -615,7 +612,7 @@ POST /projects/:id/unstar | `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/unstar" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/unstar" ``` Example response: @@ -625,8 +622,7 @@ Example response: "id": 3, "description": null, "default_branch": "master", - "public": false, - "visibility_level": 10, + "visibility": "internal", "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git", "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", "web_url": "http://example.com/diaspora/diaspora-project-site", @@ -662,7 +658,7 @@ Example response: "star_count": 0, "public_builds": true, "shared_with_groups": [], - "only_allow_merge_if_build_succeeds": false, + "only_allow_merge_if_pipeline_succeeds": false, "only_allow_merge_if_all_discussions_are_resolved": false, "request_access_enabled": false } @@ -682,7 +678,7 @@ POST /projects/:id/archive | `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/archive" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/archive" ``` Example response: @@ -692,8 +688,7 @@ Example response: "id": 3, "description": null, "default_branch": "master", - "public": false, - "visibility_level": 0, + "visibility": "private", "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git", "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", "web_url": "http://example.com/diaspora/diaspora-project-site", @@ -745,7 +740,7 @@ Example response: "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b", "public_builds": true, "shared_with_groups": [], - "only_allow_merge_if_build_succeeds": false, + "only_allow_merge_if_pipeline_succeeds": false, "only_allow_merge_if_all_discussions_are_resolved": false, "request_access_enabled": false } @@ -765,7 +760,7 @@ POST /projects/:id/unarchive | `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/unarchive" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/unarchive" ``` Example response: @@ -775,8 +770,7 @@ Example response: "id": 3, "description": null, "default_branch": "master", - "public": false, - "visibility_level": 0, + "visibility": "private", "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git", "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", "web_url": "http://example.com/diaspora/diaspora-project-site", @@ -828,7 +822,7 @@ Example response: "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b", "public_builds": true, "shared_with_groups": [], - "only_allow_merge_if_build_succeeds": false, + "only_allow_merge_if_pipeline_succeeds": false, "only_allow_merge_if_all_discussions_are_resolved": false, "request_access_enabled": false } @@ -914,7 +908,7 @@ Parameters: | `group_id` | integer | yes | The ID of the group | ```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/share/17 +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/share/17 ``` ## Hooks diff --git a/doc/api/repositories.md b/doc/api/repositories.md index 727617f1ecc..ddd11bb2a14 100644 --- a/doc/api/repositories.md +++ b/doc/api/repositories.md @@ -5,6 +5,8 @@ Get a list of repository files and directories in a project. This endpoint can be accessed without authentication if the repository is publicly accessible. +This command provides essentially the same functionality as the `git ls-tree` command. For more information, see the section _Tree Objects_ in the [Git internals documentation](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects/#_tree_objects). + ``` GET /projects/:id/repository/tree ``` diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md index 677e209ccd9..ec56d0efa1c 100644 --- a/doc/api/repository_files.md +++ b/doc/api/repository_files.md @@ -15,7 +15,7 @@ GET /projects/:id/repository/files ``` ```bash -curl --request GET --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/models/key.rb&ref=master' +curl --request GET --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files?file_path=app/models/key.rb&ref=master' ``` Example response: @@ -46,7 +46,7 @@ POST /projects/:id/repository/files ``` ```bash -curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20content&commit_message=create%20a%20new%20file' +curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files?file_path=app/project.rb&branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20content&commit_message=create%20a%20new%20file' ``` Example response: @@ -75,7 +75,7 @@ PUT /projects/:id/repository/files ``` ```bash -curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20other%20content&commit_message=update%20file' +curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files?file_path=app/project.rb&branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20other%20content&commit_message=update%20file' ``` Example response: @@ -113,7 +113,7 @@ DELETE /projects/:id/repository/files ``` ```bash -curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file' +curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files?file_path=app/project.rb&branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file' ``` Example response: diff --git a/doc/api/runners.md b/doc/api/runners.md index 28610762dca..46f882ce937 100644 --- a/doc/api/runners.md +++ b/doc/api/runners.md @@ -18,7 +18,7 @@ GET /runners?scope=active | `scope` | string | no | The scope of specific runners to show, one of: `active`, `paused`, `online`; showing all runners if none provided | ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners" ``` Example response: @@ -57,7 +57,7 @@ GET /runners/all?scope=online | `scope` | string | no | The scope of runners to show, one of: `specific`, `shared`, `active`, `paused`, `online`; showing all runners if none provided | ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/all" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners/all" ``` Example response: @@ -108,7 +108,7 @@ GET /runners/:id | `id` | integer | yes | The ID of a runner | ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners/6" ``` Example response: @@ -158,7 +158,7 @@ PUT /runners/:id | `tag_list` | array | no | The list of tags for a runner; put array of tags, that should be finally assigned to a runner | ``` -curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2" +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners/6" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2" ``` Example response: @@ -207,19 +207,7 @@ DELETE /runners/:id | `id` | integer | yes | The ID of a runner | ``` -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6" -``` - -Example response: - -```json -{ - "active": true, - "description": "test-1-20150125-test", - "id": 6, - "is_shared": false, - "name": null, -} +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners/6" ``` ## List project's runners @@ -237,7 +225,7 @@ GET /projects/:id/runners | `id` | integer | yes | The ID of a project | ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/9/runners" ``` Example response: @@ -275,7 +263,7 @@ POST /projects/:id/runners | `runner_id` | integer | yes | The ID of a runner | ``` -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners" --form "runner_id=9" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/9/runners" --form "runner_id=9" ``` Example response: @@ -306,17 +294,5 @@ DELETE /projects/:id/runners/:runner_id | `runner_id` | integer | yes | The ID of a runner | ``` -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners/9" -``` - -Example response: - -```json -{ - "active": true, - "description": "test-2016-02-01", - "id": 9, - "is_shared": false, - "name": null -} +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/9/runners/9" ``` diff --git a/doc/api/session.md b/doc/api/session.md index d7809716fbe..056cc32597c 100644 --- a/doc/api/session.md +++ b/doc/api/session.md @@ -21,7 +21,7 @@ POST /session | `password` | string | yes | The password of the user | ```bash -curl --request POST "https://gitlab.example.com/api/v3/session?login=john_smith&password=strongpassw0rd" +curl --request POST "https://gitlab.example.com/api/v4/session?login=john_smith&password=strongpassw0rd" ``` Example response: diff --git a/doc/api/settings.md b/doc/api/settings.md index ca6b9347877..38a37cd920c 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -13,7 +13,7 @@ GET /application/settings ``` ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/application/settings ``` Example response: @@ -32,12 +32,13 @@ Example response: "updated_at" : "2016-01-04T15:44:55.176Z", "session_expire_delay" : 10080, "home_page_url" : null, - "default_snippet_visibility" : 0, + "default_snippet_visibility" : "private", "domain_whitelist" : [], "domain_blacklist_enabled" : false, "domain_blacklist" : [], "created_at" : "2016-01-04T15:44:55.176Z", - "default_project_visibility" : 0, + "default_project_visibility" : "private", + "default_group_visibility" : "private", "gravatar_enabled" : true, "sign_in_text" : null, "container_registry_token_expire_delay": 5, @@ -66,11 +67,12 @@ PUT /application/settings | `sign_in_text` | string | no | Text on login page | | `home_page_url` | string | no | Redirect to this URL when not logged in | | `default_branch_protection` | integer | no | Determine if developers can push to master. Can take `0` _(not protected, both developers and masters can push new commits, force push or delete the branch)_, `1` _(partially protected, developers can push new commits, but cannot force push or delete the branch, masters can do anything)_ or `2` _(fully protected, developers cannot push new commits, force push or delete the branch, masters can do anything)_ as a parameter. Default is `2`. | -| `restricted_visibility_levels` | array of integers | no | Selected levels cannot be used by non-admin users for projects or snippets. Can take `0` _(Private)_, `1` _(Internal)_ and `2` _(Public)_ as a parameter. Default is null which means there is no restriction. | +| `restricted_visibility_levels` | array of strings | no | Selected levels cannot be used by non-admin users for projects or snippets. Can take `private`, `internal` and `public` as a parameter. Default is null which means there is no restriction. | | `max_attachment_size` | integer | no | Limit attachment size in MB | | `session_expire_delay` | integer | no | Session duration in minutes. GitLab restart is required to apply changes | -| `default_project_visibility` | integer | no | What visibility level new projects receive. Can take `0` _(Private)_, `1` _(Internal)_ and `2` _(Public)_ as a parameter. Default is `0`.| -| `default_snippet_visibility` | integer | no | What visibility level new snippets receive. Can take `0` _(Private)_, `1` _(Internal)_ and `2` _(Public)_ as a parameter. Default is `0`.| +| `default_project_visibility` | string | no | What visibility level new projects receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`.| +| `default_snippet_visibility` | string | no | What visibility level new snippets receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`.| +| `default_group_visibility` | string | no | What visibility level new groups receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`.| | `domain_whitelist` | array of strings | no | Force people to use only corporate emails for sign-up. Default is null, meaning there is no restriction. | | `domain_blacklist_enabled` | boolean | no | Enable/disable the `domain_blacklist` | | `domain_blacklist` | array of strings | yes (if `domain_blacklist_enabled` is `true`) | People trying to sign-up with emails from this domain will not be allowed to do so. | @@ -88,7 +90,7 @@ PUT /application/settings | `terminal_max_session_time` | integer | no | Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time. | ```bash -curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings?signup_enabled=false&default_project_visibility=1 +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/application/settings?signup_enabled=false&default_project_visibility=internal ``` Example response: @@ -108,8 +110,9 @@ Example response: "restricted_visibility_levels": [], "max_attachment_size": 10, "session_expire_delay": 10080, - "default_project_visibility": 1, - "default_snippet_visibility": 0, + "default_project_visibility": "internal", + "default_snippet_visibility": "private", + "default_group_visibility": "private", "domain_whitelist": [], "domain_blacklist_enabled" : false, "domain_blacklist" : [], diff --git a/doc/api/sidekiq_metrics.md b/doc/api/sidekiq_metrics.md index 1ae732d40d6..ea10a26bcd0 100644 --- a/doc/api/sidekiq_metrics.md +++ b/doc/api/sidekiq_metrics.md @@ -15,7 +15,7 @@ GET /sidekiq/queue_metrics ``` ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/queue_metrics +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/sidekiq/queue_metrics ``` Example response: @@ -40,7 +40,7 @@ GET /sidekiq/process_metrics ``` ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/process_metrics +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/sidekiq/process_metrics ``` Example response: @@ -82,7 +82,7 @@ GET /sidekiq/job_stats ``` ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/job_stats +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/sidekiq/job_stats ``` Example response: @@ -106,7 +106,7 @@ GET /sidekiq/compound_metrics ``` ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/compound_metrics +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/sidekiq/compound_metrics ``` Example response: diff --git a/doc/api/snippets.md b/doc/api/snippets.md index 5a5dc162ffe..e09d930698e 100644 --- a/doc/api/snippets.md +++ b/doc/api/snippets.md @@ -5,15 +5,15 @@ ### Snippet visibility level Snippets in GitLab can be either private, internal, or public. -You can set it with the `visibility_level` field in the snippet. +You can set it with the `visibility` field in the snippet. Constants for snippet visibility levels are: -| Visibility | Visibility level | Description | -| ---------- | ---------------- | ----------- | -| Private | `0` | The snippet is visible only to the snippet creator | -| Internal | `10` | The snippet is visible for any logged in user | -| Public | `20` | The snippet can be accessed without any authentication | +| Visibility | Description | +| ---------- | ----------- | +| `private` | The snippet is visible only to the snippet creator | +| `internal` | The snippet is visible for any logged in user | +| `public` | The snippet can be accessed without any authentication | ## List snippets @@ -38,7 +38,7 @@ Parameters: | `id` | Integer | yes | The ID of a snippet | ``` bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets/1 +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/snippets/1 ``` Example response: @@ -78,11 +78,11 @@ Parameters: | `title` | String | yes | The title of a snippet | | `file_name` | String | yes | The name of a snippet file | | `content` | String | yes | The content of a snippet | -| `visibility_level` | Integer | yes | The snippet's visibility | +| `visibility` | String | yes | The snippet's visibility | ``` bash -curl --request POST --data '{"title": "This is a snippet", "content": "Hello world", "file_name": "test.txt", "visibility_level": 10 }' --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets +curl --request POST --data '{"title": "This is a snippet", "content": "Hello world", "file_name": "test.txt", "visibility": "internal" }' --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/snippets ``` Example response: @@ -123,11 +123,11 @@ Parameters: | `title` | String | no | The title of a snippet | | `file_name` | String | no | The name of a snippet file | | `content` | String | no | The content of a snippet | -| `visibility_level` | Integer | no | The snippet's visibility | +| `visibility` | String | no | The snippet's visibility | ``` bash -curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data '{"title": "foo", "content": "bar"}' https://gitlab.example.com/api/v3/snippets/1 +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data '{"title": "foo", "content": "bar"}' https://gitlab.example.com/api/v4/snippets/1 ``` Example response: @@ -154,7 +154,7 @@ Example response: ## Delete snippet -Deletes an existing snippet. +Deletes an existing snippet. ``` DELETE /snippets/:id @@ -168,7 +168,7 @@ Parameters: ``` -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/snippets/1" +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/snippets/1" ``` upon successful delete a `204 No content` HTTP code shall be expected, with no data, @@ -186,7 +186,7 @@ GET /snippets/public | `page` | Integer | no | the page to retrieve | ``` bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets/public?per_page=2&page=1 +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/snippets/public?per_page=2&page=1 ``` Example response: @@ -229,4 +229,3 @@ Example response: } ] ``` - diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md index 3fb8b73be6d..bad380794c1 100644 --- a/doc/api/system_hooks.md +++ b/doc/api/system_hooks.md @@ -20,7 +20,7 @@ GET /hooks Example request: ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/hooks ``` Example response: @@ -59,7 +59,7 @@ POST /hooks Example request: ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/hooks?url=https://gitlab.example.com/hook" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/hooks?url=https://gitlab.example.com/hook" ``` Example response: @@ -90,7 +90,7 @@ GET /hooks/:id Example request: ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2 +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/hooks/2 ``` Example response: @@ -123,24 +123,5 @@ DELETE /hooks/:id Example request: ```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2 -``` - -Example response: - -```json -{ - "note_events" : false, - "project_id" : null, - "enable_ssl_verification" : true, - "url" : "https://gitlab.example.com/hook", - "updated_at" : "2015-11-04T20:12:15.931Z", - "issues_events" : false, - "merge_requests_events" : false, - "created_at" : "2015-11-04T20:12:15.931Z", - "service_id" : null, - "id" : 2, - "push_events" : true, - "tag_push_events" : false -} +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/hooks/2 ``` diff --git a/doc/api/tags.md b/doc/api/tags.md index 7f78ffc2390..bf350f024f5 100644 --- a/doc/api/tags.md +++ b/doc/api/tags.md @@ -26,7 +26,7 @@ Parameters: "committer_email": "jack@example.com", "id": "2695effb5807a22ff3d138d593fd856244e155e7", "message": "Initial commit", - "parents_ids": [ + "parent_ids": [ "2a4b78934375d7f53875269ffd4f45fd83a84ebe" ] }, @@ -57,7 +57,7 @@ Parameters: | `tag_name` | string | yes | The name of the tag | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/tags/v1.0.0 +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/repository/tags/v1.0.0 ``` Example Response: @@ -110,7 +110,7 @@ Parameters: "committer_email": "jack@example.com", "id": "2695effb5807a22ff3d138d593fd856244e155e7", "message": "Initial commit", - "parents_ids": [ + "parent_ids": [ "2a4b78934375d7f53875269ffd4f45fd83a84ebe" ] }, @@ -141,11 +141,6 @@ Parameters: - `id` (required) - The ID of a project - `tag_name` (required) - The name of a tag -```json -{ - "tag_name": "v4.3.0" -} -``` ## Create a new release diff --git a/doc/api/templates/gitignores.md b/doc/api/templates/gitignores.md index 8235be92b12..3f2f4ed54e0 100644 --- a/doc/api/templates/gitignores.md +++ b/doc/api/templates/gitignores.md @@ -9,7 +9,7 @@ GET /templates/gitignores ``` ```bash -curl https://gitlab.example.com/api/v3/templates/gitignores +curl https://gitlab.example.com/api/v4/templates/gitignores ``` Example response: @@ -566,7 +566,7 @@ GET /templates/gitignores/:key | `key` | string | yes | The key of the gitignore template | ```bash -curl https://gitlab.example.com/api/v3/templates/gitignores/Ruby +curl https://gitlab.example.com/api/v4/templates/gitignores/Ruby ``` Example response: diff --git a/doc/api/templates/gitlab_ci_ymls.md b/doc/api/templates/gitlab_ci_ymls.md index e120016fbe6..27e8973da58 100644 --- a/doc/api/templates/gitlab_ci_ymls.md +++ b/doc/api/templates/gitlab_ci_ymls.md @@ -9,7 +9,7 @@ GET /templates/gitlab_ci_ymls ``` ```bash -curl https://gitlab.example.com/api/v3/templates/gitlab_ci_ymls +curl https://gitlab.example.com/api/v4/templates/gitlab_ci_ymls ``` Example response: @@ -107,7 +107,7 @@ GET /templates/gitlab_ci_ymls/:key | `key` | string | yes | The key of the GitLab CI YML template | ```bash -curl https://gitlab.example.com/api/v3/templates/gitlab_ci_ymls/Ruby +curl https://gitlab.example.com/api/v4/templates/gitlab_ci_ymls/Ruby ``` Example response: diff --git a/doc/api/templates/licenses.md b/doc/api/templates/licenses.md index ae7218cf1bd..33018f0c53f 100644 --- a/doc/api/templates/licenses.md +++ b/doc/api/templates/licenses.md @@ -13,7 +13,7 @@ GET /templates/licenses | `popular` | boolean | no | If passed, returns only popular licenses | ```bash -curl https://gitlab.example.com/api/v3/templates/licenses?popular=1 +curl https://gitlab.example.com/api/v4/templates/licenses?popular=1 ``` Example response: @@ -116,7 +116,7 @@ If you omit the `fullname` parameter but authenticate your request, the name of the authenticated user will be used to replace the copyright holder placeholder. ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/templates/licenses/mit?project=My+Cool+Project +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/templates/licenses/mit?project=My+Cool+Project ``` Example response: diff --git a/doc/api/todos.md b/doc/api/todos.md index a2fbbc7e1f8..77667a57195 100644 --- a/doc/api/todos.md +++ b/doc/api/todos.md @@ -22,7 +22,7 @@ Parameters: | `type` | string | no | The type of a todo. Can be either `Issue` or `MergeRequest` | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/todos ``` Example Response: @@ -92,7 +92,7 @@ Example Response: "updated_at": "2016-06-17T07:47:34.163Z", "due_date": null }, - "merge_when_build_succeeds": false, + "merge_when_pipeline_succeeds": false, "merge_status": "cannot_be_merged", "subscribed": true, "user_notes_count": 7 @@ -165,7 +165,7 @@ Example Response: "updated_at": "2016-06-17T07:47:34.163Z", "due_date": null }, - "merge_when_build_succeeds": false, + "merge_when_pipeline_succeeds": false, "merge_status": "cannot_be_merged", "subscribed": true, "user_notes_count": 7 @@ -194,7 +194,7 @@ Parameters: | `id` | integer | yes | The ID of a todo | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos/130/mark_as_done +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/todos/130/mark_as_done ``` Example Response: @@ -263,7 +263,7 @@ Example Response: "updated_at": "2016-06-17T07:47:34.163Z", "due_date": null }, - "merge_when_build_succeeds": false, + "merge_when_pipeline_succeeds": false, "merge_status": "cannot_be_merged", "subscribed": true, "user_notes_count": 7 @@ -284,7 +284,7 @@ POST /todos/mark_as_done ``` ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos/donmark_as_donee +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/todos/donmark_as_donee ``` diff --git a/doc/api/users.md b/doc/api/users.md index d14548e8bbb..95f6bcfccb6 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -699,7 +699,7 @@ Parameters: | `id` | integer | yes | The ID of the user | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/users/:id/events +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/:id/events ``` Example response: diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md index c178e224cc5..42deccba0a6 100644 --- a/doc/api/v3_to_v4.md +++ b/doc/api/v3_to_v4.md @@ -28,8 +28,14 @@ changes are in V4: - `/dockerfiles/:key` - Moved `/projects/fork/:id` to `/projects/:id/fork` [!8940](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8940) - Moved `DELETE /todos` to `POST /todos/mark_as_done` and `DELETE /todos/:todo_id` to `POST /todos/:todo_id/mark_as_done` [!9410](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9410) -- Endpoints `/projects/owned`, `/projects/visible`, `/projects/starred` & `/projects/all` are consolidated into `/projects` using query parameters [!8962](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8962) +- Project filters are no longer available as `GET /projects/foo`, but as `GET /projects?foo=true` instead [!8962](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8962) + - `GET /projects/visible` & `GET /projects/all` are consolidated into `GET /projects` and can be used with or without authorization + - `GET /projects/owned` moved to `GET /projects?owned=true` + - `GET /projects/starred` moved to `GET /projects?starred=true` +- `GET /projects` returns all projects visible to current user, even if the user is not a member [!9674](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9674) + - To get projects the user is a member of, use `/projects?membership=true` - Return pagination headers for all endpoints that return an array [!8606](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8606) +- Added `POST /environments/:environment_id/stop` to stop an environment [!8808](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8808) - Removed `DELETE projects/:id/deploy_keys/:key_id/disable`. Use `DELETE projects/:id/deploy_keys/:key_id` instead [!9366](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9366) - Moved `PUT /users/:id/(block|unblock)` to `POST /users/:id/(block|unblock)` [!9371](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9371) - Make subscription API more RESTful. Use `post ":project_id/:subscribable_type/:subscribable_id/subscribe"` to subscribe and `post ":project_id/:subscribable_type/:subscribable_id/unsubscribe"` to unsubscribe from a resource. [!9325](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9325) @@ -38,9 +44,17 @@ changes are in V4: - POST `:id/repository/branches` - POST `:id/repository/commits` - POST/PUT/DELETE `:id/repository/files` +- Renamed `merge when build succeeds` to merge `when pipeline succeeds parameters` on the following endpoints: [!9335](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/) + - PUT `projects/:id/merge_requests/:merge_request_id/merge` + - POST `projects/:id/merge_requests/:merge_request_id/cancel_merge_when_pipeline_succeeds` + - POST `projects` + - POST `projects/user/:user_id` + - PUT `projects/:id` - Renamed `branch_name` to `branch` on DELETE `id/repository/branches/:branch` response [!8936](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8936) - Remove `public` param from create and edit actions of projects [!8736](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8736) +- Use `visibility` as string parameter everywhere [!9337](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9337) - Notes do not return deprecated field `upvote` and `downvote` [!9384](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9384) - Return HTTP status code `400` for all validation errors when creating or updating a member instead of sometimes `422` error. [!9523](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9523) - Remove `GET /groups/owned`. Use `GET /groups?owned=true` instead [!9505](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9505) - Return 202 with JSON body on async removals on V4 API (DELETE `/projects/:id/repository/merged_branches` and DELETE `/projects/:id`) [!9449](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9449) +- `projects/:id/milestones?iid[]=x&iid[]=y` array filter has been renamed to `iids` [!9096](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9096) diff --git a/doc/api/version.md b/doc/api/version.md index 287d17cf97f..8b2a5b51bc5 100644 --- a/doc/api/version.md +++ b/doc/api/version.md @@ -10,7 +10,7 @@ GET /version ``` ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/version +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/version ``` Example response: diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md index 740edba1f59..1ad9621c8a0 100644 --- a/doc/ci/triggers/README.md +++ b/doc/ci/triggers/README.md @@ -71,7 +71,7 @@ To trigger a job from webhook of another project you need to add the following webhook url for Push and Tag push events: ``` -https://gitlab.example.com/api/v3/projects/:id/ref/:ref/trigger/builds?token=TOKEN +https://gitlab.example.com/api/v4/projects/:id/ref/:ref/trigger/builds?token=TOKEN ``` > **Note**: @@ -105,7 +105,7 @@ Using cURL you can trigger a rebuild with minimal effort, for example: curl --request POST \ --form token=TOKEN \ --form ref=master \ - https://gitlab.example.com/api/v3/projects/9/trigger/builds + https://gitlab.example.com/api/v4/projects/9/trigger/builds ``` In this case, the project with ID `9` will get rebuilt on `master` branch. @@ -114,7 +114,7 @@ Alternatively, you can pass the `token` and `ref` arguments in the query string: ```bash curl --request POST \ - "https://gitlab.example.com/api/v3/projects/9/trigger/builds?token=TOKEN&ref=master" + "https://gitlab.example.com/api/v4/projects/9/trigger/builds?token=TOKEN&ref=master" ``` ### Triggering a job within `.gitlab-ci.yml` @@ -128,7 +128,7 @@ need to add in project's A `.gitlab-ci.yml`: build_docs: stage: deploy script: - - "curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v3/projects/9/trigger/builds" + - "curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v4/projects/9/trigger/builds" only: - tags ``` @@ -187,7 +187,7 @@ curl --request POST \ --form token=TOKEN \ --form ref=master \ --form "variables[UPLOAD_TO_S3]=true" \ - https://gitlab.example.com/api/v3/projects/9/trigger/builds + https://gitlab.example.com/api/v4/projects/9/trigger/builds ``` ### Using webhook to trigger job @@ -195,7 +195,7 @@ curl --request POST \ You can add the following webhook to another project in order to trigger a job: ``` -https://gitlab.example.com/api/v3/projects/9/ref/master/trigger/builds?token=TOKEN&variables[UPLOAD_TO_S3]=true +https://gitlab.example.com/api/v4/projects/9/ref/master/trigger/builds?token=TOKEN&variables[UPLOAD_TO_S3]=true ``` ### Using cron to trigger nightly jobs @@ -205,7 +205,7 @@ in conjunction with cron. The example below triggers a job on the `master` branch of project with ID `9` every night at `00:30`: ```bash -30 0 * * * curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v3/projects/9/trigger/builds +30 0 * * * curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v4/projects/9/trigger/builds ``` [ci-229]: https://gitlab.com/gitlab-org/gitlab-ci/merge_requests/229 diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 620d4744685..04c0af44237 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -131,6 +131,16 @@ job_name: variables: [] ``` +You are able to use other variables inside your variable definition (or escape them with `$$`): + +```yaml +variables: + LS_CMD: 'ls $FLAGS $$TMP_DIR' + FLAGS: '-al' +script: + - 'eval $LS_CMD' # will execute 'ls -al $TMP_DIR' +``` + ## Secret variables >**Notes:** diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md index fc948a7a116..9bed441c131 100644 --- a/doc/development/doc_styleguide.md +++ b/doc/development/doc_styleguide.md @@ -444,7 +444,7 @@ Rendered example: ### cURL commands -- Use `https://gitlab.example.com/api/v3/` as an endpoint. +- Use `https://gitlab.example.com/api/v4/` as an endpoint. - Wherever needed use this private token: `9koXpg98eAheJpvBs5tK`. - Always put the request first. `GET` is the default so you don't have to include it. @@ -468,7 +468,7 @@ Below is a set of [cURL][] examples that you can use in the API documentation. Get the details of a group: ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/gitlab-org +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/gitlab-org ``` #### cURL example with parameters passed in the URL @@ -476,7 +476,7 @@ curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/a Create a new project under the authenticated user's namespace: ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects?name=foo" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects?name=foo" ``` #### Post data using cURL's --data @@ -486,7 +486,7 @@ cURL's `--data` option. The example below will create a new project `foo` under the authenticated user's namespace. ```bash -curl --data "name=foo" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects" +curl --data "name=foo" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects" ``` #### Post data using JSON content @@ -495,7 +495,7 @@ curl --data "name=foo" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://g and double quotes. ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data '{"path": "my-group", "name": "My group"}' https://gitlab.example.com/api/v3/groups +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data '{"path": "my-group", "name": "My group"}' https://gitlab.example.com/api/v4/groups ``` #### Post data using form-data @@ -504,7 +504,7 @@ Instead of using JSON or urlencode you can use multipart/form-data which properly handles data encoding: ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "title=ssh-key" --form "key=ssh-rsa AAAAB3NzaC1yc2EA..." https://gitlab.example.com/api/v3/users/25/keys +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "title=ssh-key" --form "key=ssh-rsa AAAAB3NzaC1yc2EA..." https://gitlab.example.com/api/v4/users/25/keys ``` The above example is run by and administrator and will add an SSH public key @@ -518,7 +518,7 @@ contains spaces in its title. Observe how spaces are escaped using the `%20` ASCII code. ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/42/issues?title=Hello%20Dude" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/42/issues?title=Hello%20Dude" ``` Use `%2F` for slashes (`/`). @@ -530,7 +530,7 @@ restrict the sign-up e-mail domains of a GitLab instance to `*.example.com` and `example.net`, you would do something like this: ```bash -curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "domain_whitelist[]=*.example.com" --data "domain_whitelist[]=example.net" https://gitlab.example.com/api/v3/application/settings +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "domain_whitelist[]=*.example.com" --data "domain_whitelist[]=example.net" https://gitlab.example.com/api/v4/application/settings ``` [cURL]: http://curl.haxx.se/ "cURL website" diff --git a/doc/development/frontend.md b/doc/development/frontend.md index ba47998de49..9ba820eaee5 100644 --- a/doc/development/frontend.md +++ b/doc/development/frontend.md @@ -238,6 +238,9 @@ readability. See the relevant style guides for our guidelines and for information on linting: - [SCSS][scss-style-guide] +- JavaScript - We defer to [AirBnb][airbnb-js-style-guide] on most style-related +conventions and enforce them with eslint. See [our current .eslintrc][eslistrc] +for specific rules and patterns. ## Testing @@ -434,3 +437,5 @@ Scenario: Developer can approve merge request [state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch [vue-resource-repo]: https://github.com/pagekit/vue-resource [issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6 +[airbnb-js-style-guide]: https://github.com/airbnb/javascript +[eslintrc]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.eslintrc diff --git a/doc/development/limit_ee_conflicts.md b/doc/development/limit_ee_conflicts.md index 2d82b09f301..51b4b398f2c 100644 --- a/doc/development/limit_ee_conflicts.md +++ b/doc/development/limit_ee_conflicts.md @@ -50,6 +50,15 @@ Notes: asking a GitLab developer to do it once the merge request is merged. - If you branch is more than 500 commits behind `master`, the job will fail and you should rebase your branch upon latest `master`. +- Code reviews for merge requests often consist of multiple iterations of + feedback and fixes. There is no need to update your EE MR after each + iteration. Instead, create an EE MR as soon as you see the + `rake ee_compat_check` job failing. After you receive the final acceptance + from a Maintainer (but before the CE MR is merged) update the EE MR. + This helps to identify significant conflicts sooner, but also reduces the + number of times you have to resolve conflicts. +- You can use [`git rerere`](https://git-scm.com/blog/2010/03/08/rerere.html) + to avoid resolving the same conflicts multiple times. ## Possible type of conflicts diff --git a/doc/development/ux_guide/img/karolina-plaskaty.png b/doc/development/ux_guide/img/karolina-plaskaty.png Binary files differnew file mode 100644 index 00000000000..2e356c99762 --- /dev/null +++ b/doc/development/ux_guide/img/karolina-plaskaty.png diff --git a/doc/development/ux_guide/img/nazim-ramesh.png b/doc/development/ux_guide/img/nazim-ramesh.png Binary files differnew file mode 100644 index 00000000000..01ba0391630 --- /dev/null +++ b/doc/development/ux_guide/img/nazim-ramesh.png diff --git a/doc/development/ux_guide/users.md b/doc/development/ux_guide/users.md index 137154e24f3..cbd7c17de41 100644 --- a/doc/development/ux_guide/users.md +++ b/doc/development/ux_guide/users.md @@ -14,7 +14,7 @@ ### Nazim Ramesh - Small to medium size organisations using GitLab CE -<img src="img/steven-lyons.png" width="300px"> +<img src="img/nazim-ramesh.png" width="300px"> #### Demographics @@ -131,7 +131,7 @@ James and his team use CI quite heavily for several projects. Whilst they’ve w - Would like to use GitLab at work - Working for a medium to large size organisation -<img src="img/harry-robison.png" width="300px"> +<img src="img/karolina-plaskaty.png" width="300px"> #### Demographics diff --git a/doc/gitlab-basics/img/create_new_project_button.png b/doc/gitlab-basics/img/create_new_project_button.png Binary files differindex a19f0e57b56..8d7a69e55ed 100644 --- a/doc/gitlab-basics/img/create_new_project_button.png +++ b/doc/gitlab-basics/img/create_new_project_button.png diff --git a/doc/install/README.md b/doc/install/README.md index 2d2fd8cb380..d35709266e4 100644 --- a/doc/install/README.md +++ b/doc/install/README.md @@ -1,9 +1,32 @@ # Installation -- [Installation](installation.md) -- [Requirements](requirements.md) -- [Structure](structure.md) -- [Database MySQL](database_mysql.md) -- [Digital Ocean and Docker](digitaloceandocker.md) -- [Docker](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/docker) -- [All installation methods](https://about.gitlab.com/installation/) +GitLab can be installed via various ways. Check the [installation methods][methods] +for an overview. + +## Requirements + +Before installing GitLab, make sure to check the [requirements documentation](requirements.md) +which includes useful information on the supported Operating Systems as well as +the hardware requirements. + +## Installation methods + +- [Installation using the Omnibus packages](https://about.gitlab.com/downloads/) - + Install GitLab using our official deb/rpm repositories. This is the + recommended way. +- [Installation from source](installation.md) - Install GitLab from source. + Useful for unsupported systems like *BSD. For an overview of the directory + structure, read the [structure documentation](structure.md). +- [Docker](https://docs.gitlab.com/omnibus/docker/) - Install GitLab using Docker. +- [Installation on Google Cloud Platform](google_cloud_platform/index.md) - Install + GitLab on Google Cloud Platform using our official image. +- [Digital Ocean and Docker](digitaloceandocker.md) - Install GitLab quickly + on DigitalOcean using Docker. + +## Database + +While the recommended database is PostgreSQL, we provide information to install +GitLab using MySQL. Check the [MySQL documentation](database_mysql.md) for more +information. + +[methods]: https://about.gitlab.com/installation/ diff --git a/doc/install/google_cloud_platform/img/change_admin_passwd_email.png b/doc/install/google_cloud_platform/img/change_admin_passwd_email.png Binary files differnew file mode 100644 index 00000000000..1ffe14f60ff --- /dev/null +++ b/doc/install/google_cloud_platform/img/change_admin_passwd_email.png diff --git a/doc/install/google_cloud_platform/img/chrome_not_secure_page.png b/doc/install/google_cloud_platform/img/chrome_not_secure_page.png Binary files differnew file mode 100644 index 00000000000..e732066908f --- /dev/null +++ b/doc/install/google_cloud_platform/img/chrome_not_secure_page.png diff --git a/doc/install/google_cloud_platform/img/gcp_gitlab_being_deployed.png b/doc/install/google_cloud_platform/img/gcp_gitlab_being_deployed.png Binary files differnew file mode 100644 index 00000000000..2a1859da6e3 --- /dev/null +++ b/doc/install/google_cloud_platform/img/gcp_gitlab_being_deployed.png diff --git a/doc/install/google_cloud_platform/img/gcp_gitlab_overview.png b/doc/install/google_cloud_platform/img/gcp_gitlab_overview.png Binary files differnew file mode 100644 index 00000000000..1c4c870dbc9 --- /dev/null +++ b/doc/install/google_cloud_platform/img/gcp_gitlab_overview.png diff --git a/doc/install/google_cloud_platform/img/gcp_landing.png b/doc/install/google_cloud_platform/img/gcp_landing.png Binary files differnew file mode 100644 index 00000000000..6398d247ba0 --- /dev/null +++ b/doc/install/google_cloud_platform/img/gcp_landing.png diff --git a/doc/install/google_cloud_platform/img/gcp_launcher_console_home_page.png b/doc/install/google_cloud_platform/img/gcp_launcher_console_home_page.png Binary files differnew file mode 100644 index 00000000000..f492888ea4a --- /dev/null +++ b/doc/install/google_cloud_platform/img/gcp_launcher_console_home_page.png diff --git a/doc/install/google_cloud_platform/img/gcp_search_for_gitlab.png b/doc/install/google_cloud_platform/img/gcp_search_for_gitlab.png Binary files differnew file mode 100644 index 00000000000..b38af3966e2 --- /dev/null +++ b/doc/install/google_cloud_platform/img/gcp_search_for_gitlab.png diff --git a/doc/install/google_cloud_platform/img/gitlab_deployed_page.png b/doc/install/google_cloud_platform/img/gitlab_deployed_page.png Binary files differnew file mode 100644 index 00000000000..fef9ae45f32 --- /dev/null +++ b/doc/install/google_cloud_platform/img/gitlab_deployed_page.png diff --git a/doc/install/google_cloud_platform/img/gitlab_first_sign_in.png b/doc/install/google_cloud_platform/img/gitlab_first_sign_in.png Binary files differnew file mode 100644 index 00000000000..381c0fe48a5 --- /dev/null +++ b/doc/install/google_cloud_platform/img/gitlab_first_sign_in.png diff --git a/doc/install/google_cloud_platform/img/gitlab_launch_button.png b/doc/install/google_cloud_platform/img/gitlab_launch_button.png Binary files differnew file mode 100644 index 00000000000..50f66f66118 --- /dev/null +++ b/doc/install/google_cloud_platform/img/gitlab_launch_button.png diff --git a/doc/install/google_cloud_platform/img/new_gitlab_deployment_settings.png b/doc/install/google_cloud_platform/img/new_gitlab_deployment_settings.png Binary files differnew file mode 100644 index 00000000000..00060841619 --- /dev/null +++ b/doc/install/google_cloud_platform/img/new_gitlab_deployment_settings.png diff --git a/doc/install/google_cloud_platform/img/ssh_via_button.png b/doc/install/google_cloud_platform/img/ssh_via_button.png Binary files differnew file mode 100644 index 00000000000..26106f159ad --- /dev/null +++ b/doc/install/google_cloud_platform/img/ssh_via_button.png diff --git a/doc/install/google_cloud_platform/index.md b/doc/install/google_cloud_platform/index.md new file mode 100644 index 00000000000..26506111548 --- /dev/null +++ b/doc/install/google_cloud_platform/index.md @@ -0,0 +1,168 @@ +# Installing GitLab on Google Cloud Platform + +![GCP landing page](img/gcp_landing.png) + +The fastest way to get started on [Google Cloud Platform (GCP)][gcp] is through +the [Google Cloud Launcher][launcher] program. + +## Prerequisites + +There are only two prerequisites in order to install GitLab on GCP: + +1. You need to have a Google account. +1. You need to sign up for the GCP program. If this is your first time, Google + gives you [$300 credit for free][freetrial] to consume over a 60-day period. + +Once you have performed those two steps, you can visit the +[GCP launcher console][console] which has a list of all the things you can +deploy on GCP. + +![GCP launcher console](img/gcp_launcher_console_home_page.png) + +The next step is to find and install GitLab. + +## Configuring and deploying the VM + +To deploy GitLab on GCP you need to follow five simple steps: + +1. Go to https://cloud.google.com/launcher and login with your Google credentials +1. Search for GitLab from GitLab Inc. (not the same as Bitnami) and click on + the tile. + + ![Search for GitLab](img/gcp_search_for_gitlab.png) + +1. In the next page, you can see an overview of the GitLab VM as well as some + estimated costs. Click the **Launch on Compute Engine** button to choose the + hardware and network settings. + + ![Launch on Compute Engine](img/gcp_gitlab_overview.png) + +1. In the settings page you can choose things like the datacenter where your GitLab + server will be hosted, the number of CPUs and amount of RAM, the disk size + and type, etc. Read GitLab's [requirements documentation][req] for more + details on what to choose depending on your needs. + + ![Deploy settings](img/new_gitlab_deployment_settings.png) + +1. As a last step, hit **Deploy** when ready. The process will finish in a few + seconds. + + ![Deploy in progress](img/gcp_gitlab_being_deployed.png) + + +## Visiting GitLab for the first time + +After a few seconds, GitLab will be successfully deployed and you should be +able to see the IP address that Google assigned to the VM, as well as the +credentials to the GitLab admin account. + +![Deploy settings](img/gitlab_deployed_page.png) + +1. Click on the IP under **Site address** to visit GitLab. +1. Accept the self-signed certificate that Google automatically deployed in + order to securely reach GitLab's login page. +1. Use the username and password that are present in the Google console page + to login into GitLab and click **Sign in**. + + ![GitLab first sign in](img/gitlab_first_sign_in.png) + +Congratulations! GitLab is now installed and you can access it via your browser, +but we're not done yet. There are some steps you need to take in order to have +a fully functional GitLab installation. + +## Next steps + +These are the most important next steps to take after you installed GitLab for +the first time. + +### Changing the admin password and email + +Google assigned a random password for the GitLab admin account and you should +change it ASAP: + +1. Visit the GitLab admin page through the link in the Google console under + **Admin URL**. +1. Find the Administrator user under the **Users** page and hit **Edit**. +1. Change the email address to a real one and enter a new password. + + ![Change GitLab admin password](img/change_admin_passwd_email.png) + +1. Hit **Save changes** for the changes to take effect. +1. After changing the password, you will be signed out from GitLab. Use the + new credentials to login again. + +### Assigning a static IP + +By default, Google assigns an ephemeral IP to your instance. It is strongly +recommended to assign a static IP if you are going to use GitLab in production +and use a domain name as we'll see below. + +Read Google's documentation on how to [promote an ephemeral IP address][ip]. + +### Using a domain name + +Assuming you have a domain name in your possession and you have correctly +set up DNS to point to the static IP you configured in the previous step, +here's how you configure GitLab to be aware of the change: + +1. SSH into the VM. You can easily use the **SSH** button in the Google console + and a new window will pop up. + + ![SSH button](img/ssh_via_button.png) + + In the future you might want to set up [connecting with an SSH key][ssh] + instead. + +1. Edit the config file of Omnibus GitLab using your favorite text editor: + + ``` + sudo vim /etc/gitlab/gitlab.rb + ``` + +1. Set the `external_url` value to the domain name you wish GitLab to have + **without** `https`: + + ``` + external_url 'http://gitlab.example.com' + ``` + + We will set up HTTPS in the next step, no need to do this now. + +1. Reconfigure GitLab for the changes to take effect: + + ``` + sudo gitlab-ctl reconfigure + ``` + +1. You can now visit GitLab using the domain name. + +### Configuring HTTPS with the domain name + +Although not needed, it's strongly recommended to secure GitLab with a TLS +certificate. Follow the steps in the [Omnibus documentation][omni-ssl]. + +### Configuring the email SMTP settings + +You need to configure the email SMTP settings correctly otherwise GitLab will +not be able to send notification emails, like comments, and password changes. +Check the [Omnibus documentation][omni-smtp] how to do so. + +## Further reading + +GitLab can be configured to authenticate with other OAuth providers, LDAP, SAML, +Kerberos, etc. Here are some documents you might be interested in reading: + +- [Omnibus GitLab documentation](https://docs.gitlab.com/omnibus/) +- [Integration documentation](https://docs.gitlab.com/ce/integration/) +- [GitLab Pages configuration](https://docs.gitlab.com/ce/administration/pages/index.html) +- [GitLab Container Registry configuration](https://docs.gitlab.com/ce/administration/container_registry.html) + +[console]: https://console.cloud.google.com/launcher "GCP launcher console" +[freetrial]: https://console.cloud.google.com/freetrial "GCP free trial" +[ip]: https://cloud.google.com/compute/docs/configure-instance-ip-addresses#promote_ephemeral_ip "Configuring an Instance's IP Addresses" +[gcp]: https://cloud.google.com/ "Google Cloud Platform" +[launcher]: https://cloud.google.com/launcher/ "Google Cloud Launcher home page" +[req]: ../requirements.md "GitLab hardware and software requirements" +[ssh]: https://cloud.google.com/compute/docs/instances/connecting-to-instance "Connecting to Linux Instances" +[omni-smtp]: https://docs.gitlab.com/omnibus/settings/smtp.html#smtp-settings "Omnibus GitLab SMTP settings" +[omni-ssl]: https://docs.gitlab.com/omnibus/settings/nginx.html#enable-https "Omnibus GitLab enable HTTPS" diff --git a/doc/install/installation.md b/doc/install/installation.md index 5ba338ba7d1..bb4141c6cd3 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -155,10 +155,9 @@ page](https://golang.org/dl). ## 4. Node Since GitLab 8.17, GitLab requires the use of node >= v4.3.0 to compile -javascript assets, and starting in GitLab 9.0, yarn >= v0.17.0 is required to -manage javascript dependencies. In many distros the versions provided by the -official package repositories are out of date, so we'll need to install through -the following commands: +javascript assets, and yarn >= v0.17.0 to manage javascript dependencies. +In many distros the versions provided by the official package repositories +are out of date, so we'll need to install through the following commands: # install node v7.x curl --location https://deb.nodesource.com/setup_7.x | bash - diff --git a/doc/pages/README.md b/doc/pages/README.md new file mode 100644 index 00000000000..7878bce3f10 --- /dev/null +++ b/doc/pages/README.md @@ -0,0 +1 @@ +This document was moved to [pages/index.md](../user/project/pages/index.md). diff --git a/doc/pages/getting_started_part_one.md b/doc/pages/getting_started_part_one.md index c5b1aa4b654..1d63ccb4d2f 100644 --- a/doc/pages/getting_started_part_one.md +++ b/doc/pages/getting_started_part_one.md @@ -1,266 +1 @@ -# GitLab Pages from A to Z: Part 1 - -- **Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates** -- _[Part 2: Quick Start Guide - Setting Up GitLab Pages](getting_started_part_two.md)_ -- _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_three.md)_ - ----- - -This is a comprehensive guide, made for those who want to -publish a website with GitLab Pages but aren't familiar with -the entire process involved. - -To **enable** GitLab Pages for GitLab CE (Community Edition) -and GitLab EE (Enterprise Edition), please read the -[admin documentation](https://docs.gitlab.com/ce/administration/pages/index.html), -and/or watch this [video tutorial](https://youtu.be/dD8c7WNcc6s). - ->**Note:** -For this guide, we assume you already have GitLab Pages -server up and running for your GitLab instance. - -## What you need to know before getting started - -Before we begin, let's understand a few concepts first. - -### Static sites - -GitLab Pages only supports static websites, meaning, -your output files must be HTML, CSS, and JavaScript only. - -To create your static site, you can either hardcode in HTML, -CSS, and JS, or use a [Static Site Generator (SSG)](https://www.staticgen.com/) -to simplify your code and build the static site for you, -which is highly recommendable and much faster than hardcoding. - ---- - -- Read through this technical overview on [Static versus Dynamic Websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/) -- Understand [how modern Static Site Generators work](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/) and what you can add to your static site -- You can use [any SSG with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) -- Fork an [example project](https://gitlab.com/pages) to build your website based upon - -### GitLab Pages domain - -If you set up a GitLab Pages project on GitLab.com, -it will automatically be accessible under a -[subdomain of `namespace.pages.io`](https://docs.gitlab.com/ce/user/project/pages/). -The `namespace` is defined by your username on GitLab.com, -or the group name you created this project under. - ->**Note:** -If you use your own GitLab instance to deploy your -site with GitLab Pages, check with your sysadmin what's your -Pages wildcard domain. This guide is valid for any GitLab instance, -you just need to replace Pages wildcard domain on GitLab.com -(`*.gitlab.io`) with your own. - -#### Practical examples - -**Project Websites:** - -- You created a project called `blog` under your username `john`, -therefore your project URL is `https://gitlab.com/john/blog/`. -Once you enable GitLab Pages for this project, and build your site, -it will be available under `https://john.gitlab.io/blog/`. -- You created a group for all your websites called `websites`, -and a project within this group is called `blog`. Your project -URL is `https://gitlab.com/websites/blog/`. Once you enable -GitLab Pages for this project, the site will live under -`https://websites.gitlab.io/blog/`. - -**User and Group Websites:** - -- Under your username, `john`, you created a project called -`john.gitlab.io`. Your project URL will be `https://gitlab.com/john/john.gitlab.io`. -Once you enable GitLab Pages for your project, your website -will be published under `https://john.gitlab.io`. -- Under your group `websites`, you created a project called -`websites.gitlab.io`. your project's URL will be `https://gitlab.com/websites/websites.gitlab.io`. Once you enable GitLab Pages for your project, -your website will be published under `https://websites.gitlab.io`. - -**General example:** - -- On GitLab.com, a project site will always be available under -`https://namespace.gitlab.io/project-name` -- On GitLab.com, a user or group website will be available under -`https://namespace.gitlab.io/` -- On your GitLab instance, replace `gitlab.io` above with your -Pages server domain. Ask your sysadmin for this information. - -### DNS Records - -A Domain Name System (DNS) web service routes visitors to websites -by translating domain names (such as `www.example.com`) into the -numeric IP addresses (such as `192.0.2.1`) that computers use to -connect to each other. - -A DNS record is created to point a (sub)domain to a certain location, -which can be an IP address or another domain. In case you want to use -GitLab Pages with your own (sub)domain, you need to access your domain's -registrar control panel to add a DNS record pointing it back to your -GitLab Pages site. - -Note that **how to** add DNS records depends on which server your domain -is hosted on. Every control panel has its own place to do it. If you are -not an admin of your domain, and don't have access to your registrar, -you'll need to ask for the technical support of your hosting service -to do it for you. - -To help you out, we've gathered some instructions on how to do that -for the most popular hosting services: - -- [Amazon](http://docs.aws.amazon.com/gettingstarted/latest/swh/getting-started-configure-route53.html) -- [Bluehost](https://my.bluehost.com/cgi/help/559) -- [CloudFlare](https://support.cloudflare.com/hc/en-us/articles/200169096-How-do-I-add-A-records-) -- [cPanel](https://documentation.cpanel.net/display/ALD/Edit+DNS+Zone) -- [DreamHost](https://help.dreamhost.com/hc/en-us/articles/215414867-How-do-I-add-custom-DNS-records-) -- [Go Daddy](https://www.godaddy.com/help/add-an-a-record-19238) -- [Hostgator](http://support.hostgator.com/articles/changing-dns-records) -- [Inmotion hosting](https://my.bluehost.com/cgi/help/559) -- [Media Temple](https://mediatemple.net/community/products/dv/204403794/how-can-i-change-the-dns-records-for-my-domain) -- [Microsoft](https://msdn.microsoft.com/en-us/library/bb727018.aspx) - -If your hosting service is not listed above, you can just try to -search the web for "how to add dns record on <my hosting service>". - -#### DNS A record - -In case you want to point a root domain (`example.com`) to your -GitLab Pages site, deployed to `namespace.gitlab.io`, you need to -log into your domain's admin control panel and add a DNS `A` record -pointing your domain to Pages' server IP address. For projects on -GitLab.com, this IP is `104.208.235.32`. For projects leaving in -other GitLab instances (CE or EE), please contact your sysadmin -asking for this information (which IP address is Pages server -running on your instance). - -**Practical Example:** - -![DNS A record pointing to GitLab.com Pages server](img/dns_a_record_example.png) - -#### DNS CNAME record - -In case you want to point a subdomain (`hello-world.example.com`) -to your GitLab Pages site initially deployed to `namespace.gitlab.io`, -you need to log into your domain's admin control panel and add a DNS -`CNAME` record pointing your subdomain to your website URL -(`namespace.gitlab.io`) address. - -Notice that, despite it's a user or project website, the `CNAME` -should point to your Pages domain (`namespace.gitlab.io`), -without any `/project-name`. - -**Practical Example:** - -![DNS CNAME record pointing to GitLab.com project](img/dns_cname_record_example.png) - -#### TL;DR - -| From | DNS Record | To | -| ---- | ---------- | -- | -| domain.com | A | 104.208.235.32 | -| subdomain.domain.com | CNAME | namespace.gitlab.io | - -> **Notes**: -> -> - **Do not** use a CNAME record if you want to point your -`domain.com` to your GitLab Pages site. Use an `A` record instead. -> - **Do not** add any special chars after the default Pages -domain. E.g., **do not** point your `subdomain.domain.com` to -`namespace.gitlab.io.` or `namespace.gitlab.io/`. - -### SSL/TLS Certificates - -Every GitLab Pages project on GitLab.com will be available under -HTTPS for the default Pages domain (`*.gitlab.io`). Once you set -up your Pages project with your custom (sub)domain, if you want -it secured by HTTPS, you will have to issue a certificate for that -(sub)domain and install it on your project. - ->**Note:** -Certificates are NOT required to add to your custom -(sub)domain on your GitLab Pages project, though they are -highly recommendable. - -The importance of having any website securely served under HTTPS -is explained on the introductory section of the blog post -[Secure GitLab Pages with StartSSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/#https-a-quick-overview). - -The reason why certificates are so important is that they encrypt -the connection between the **client** (you, me, your visitors) -and the **server** (where you site lives), through a keychain of -authentications and validations. - -### Issuing Certificates - -GitLab Pages accepts [PEM](https://support.quovadisglobal.com/kb/a37/what-is-pem-format.aspx) certificates issued by -[Certificate Authorities (CA)](https://en.wikipedia.org/wiki/Certificate_authority) -and self-signed certificates. Of course, -[you'd rather issue a certificate than generate a self-signed](https://en.wikipedia.org/wiki/Self-signed_certificate), -for security reasons and for having browsers trusting your -site's certificate. - -There are several different kinds of certificates, each one -with certain security level. A static personal website will -not require the same security level as an online banking web app, -for instance. There are a couple Certificate Authorities that -offer free certificates, aiming to make the internet more secure -to everyone. The most popular is [Let's Encrypt](https://letsencrypt.org/), -which issues certificates trusted by most of browsers, it's open -source, and free to use. Please read through this tutorial to -understand [how to secure your GitLab Pages website with Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/). - -With the same popularity, there are [certificates issued by CloudFlare](https://www.cloudflare.com/ssl/), -which also offers a [free CDN service](https://blog.cloudflare.com/cloudflares-free-cdn-and-you/). -Their certs are valid up to 15 years. Read through the tutorial on -[how to add a CloudFlare Certificate to your GitLab Pages website](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/). - -### Adding certificates to your project - -Regardless the CA you choose, the steps to add your certificate to -your Pages project are the same. - -#### What do you need - -1. A PEM certificate -1. An intermediate certificate -1. A public key - -![Pages project - adding certificates](img/add_certificate_to_pages.png) - -These fields are found under your **Project**'s **Settings** > **Pages** > **New Domain**. - -#### What's what? - -- A PEM certificate is the certificate generated by the CA, -which needs to be added to the field **Certificate (PEM)**. -- An [intermediate certificate](https://en.wikipedia.org/wiki/Intermediate_certificate_authority) (aka "root certificate") is -the part of the encryption keychain that identifies the CA. -Usually it's combined with the PEM certificate, but there are -some cases in which you need to add them manually. -[CloudFlare certs](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) -are one of these cases. -- A public key is an encrypted key which validates -your PEM against your domain. - -#### Now what? - -Now that you hopefully understand why you need all -of this, it's simple: - -- Your PEM certificate needs to be added to the first field -- If your certificate is missing its intermediate, copy -and paste the root certificate (usually available from your CA website) -and paste it in the [same field as your PEM certificate](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/), -just jumping a line between them. -- Copy your public key and paste it in the last field - ->**Note:** -**Do not** open certificates or encryption keys in -regular text editors. Always use code editors (such as -Sublime Text, Atom, Dreamweaver, Brackets, etc). - -||| -|:--|--:| -||[**Part 2: Quick start guide - Setting up GitLab Pages →**](getting_started_part_two.md)| +This document was moved to [another location](../user/project/pages/getting_started_part_one.md). diff --git a/doc/pages/getting_started_part_three.md b/doc/pages/getting_started_part_three.md index ef47abef3a0..1697b5cd6b4 100644 --- a/doc/pages/getting_started_part_three.md +++ b/doc/pages/getting_started_part_three.md @@ -1,383 +1 @@ -# GitLab Pages from A to Z: Part 3 - -- _[Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](getting_started_part_one.md)_ -- _[Part 2: Quick Start Guide - Setting Up GitLab Pages](getting_started_part_two.md)_ -- **Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages** - ---- - -## Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages - -[GitLab CI](https://about.gitlab.com/gitlab-ci/) serves -numerous purposes, to build, test, and deploy your app -from GitLab through -[Continuous Integration, Continuous Delivery, and Continuous Deployment](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) -methods. You will need it to build your website with GitLab Pages, -and deploy it to the Pages server. - -What this file actually does is telling the -[GitLab Runner](https://docs.gitlab.com/runner/) to run scripts -as you would do from the command line. The Runner acts as your -terminal. GitLab CI tells the Runner which commands to run. -Both are built-in in GitLab, and you don't need to set up -anything for them to work. - -Explaining [every detail of GitLab CI](https://docs.gitlab.com/ce/ci/yaml/README.html) -and GitLab Runner is out of the scope of this guide, but we'll -need to understand just a few things to be able to write our own -`.gitlab-ci.yml` or tweak an existing one. It's an -[Yaml](http://docs.ansible.com/ansible/YAMLSyntax.html) file, -with its own syntax. You can always check your CI syntax with -the [GitLab CI Lint Tool](https://gitlab.com/ci/lint). - -**Practical Example:** - -Let's consider you have a [Jekyll](https://jekyllrb.com/) site. -To build it locally, you would open your terminal, and run `jekyll build`. -Of course, before building it, you had to install Jekyll in your computer. -For that, you had to open your terminal and run `gem install jekyll`. -Right? GitLab CI + GitLab Runner do the same thing. But you need to -write in the `.gitlab-ci.yml` the script you want to run so -GitLab Runner will do it for you. It looks more complicated then it -is. What you need to tell the Runner: - -``` -$ gem install jekyll -$ jekyll build -``` - -### Script - -To transpose this script to Yaml, it would be like this: - -```yaml -script: - - gem install jekyll - - jekyll build -``` - -### Job - -So far so good. Now, each `script`, in GitLab is organized by -a `job`, which is a bunch of scripts and settings you want to -apply to that specific task. - -```yaml -job: - script: - - gem install jekyll - - jekyll build -``` - -For GitLab Pages, this `job` has a specific name, called `pages`, -which tells the Runner you want that task to deploy your website -with GitLab Pages: - -```yaml -pages: - script: - - gem install jekyll - - jekyll build -``` - -### The `public` directory - -We also need to tell Jekyll where do you want the website to build, -and GitLab Pages will only consider files in a directory called `public`. -To do that with Jekyll, we need to add a flag specifying the -[destination (`-d`)](https://jekyllrb.com/docs/usage/) of the -built website: `jekyll build -d public`. Of course, we need -to tell this to our Runner: - -```yaml -pages: - script: - - gem install jekyll - - jekyll build -d public -``` - -### Artifacts - -We also need to tell the Runner that this _job_ generates -_artifacts_, which is the site built by Jekyll. -Where are these artifacts stored? In the `public` directory: - -```yaml -pages: - script: - - gem install jekyll - - jekyll build -d public - artifacts: - paths: - - public -``` - -The script above would be enough to build your Jekyll -site with GitLab Pages. But, from Jekyll 3.4.0 on, its default -template originated by `jekyll new project` requires -[Bundler](http://bundler.io/) to install Jekyll dependencies -and the default theme. To adjust our script to meet these new -requirements, we only need to install and build Jekyll with Bundler: - -```yaml -pages: - script: - - bundle install - - bundle exec jekyll build -d public - artifacts: - paths: - - public -``` - -That's it! A `.gitlab-ci.yml` with the content above would deploy -your Jekyll 3.4.0 site with GitLab Pages. This is the minimum -configuration for our example. On the steps below, we'll refine -the script by adding extra options to our GitLab CI. - -### Image - -At this point, you probably ask yourself: "okay, but to install Jekyll -I need Ruby. Where is Ruby on that script?". The answer is simple: the -first thing GitLab Runner will look for in your `.gitlab-ci.yml` is a -[Docker](https://www.docker.com/) image specifying what do you need in -your container to run that script: - -```yaml -image: ruby:2.3 - -pages: - script: - - bundle install - - bundle exec jekyll build -d public - artifacts: - paths: - - public -``` - -In this case, you're telling the Runner to pull this image, which -contains Ruby 2.3 as part of its file system. When you don't specify -this image in your configuration, the Runner will use a default -image, which is Ruby 2.1. - -If your SSG needs [NodeJS](https://nodejs.org/) to build, you'll -need to specify which image you want to use, and this image should -contain NodeJS as part of its file system. E.g., for a -[Hexo](https://gitlab.com/pages/hexo) site, you can use `image: node:4.2.2`. - ->**Note:** -We're not trying to explain what a Docker image is, -we just need to introduce the concept with a minimum viable -explanation. To know more about Docker images, please visit -their website or take a look at a -[summarized explanation](http://paislee.io/how-to-automate-docker-deployments/) here. - -Let's go a little further. - -### Branching - -If you use GitLab as a version control platform, you will have your -branching strategy to work on your project. Meaning, you will have -other branches in your project, but you'll want only pushes to the -default branch (usually `master`) to be deployed to your website. -To do that, we need to add another line to our CI, telling the Runner -to only perform that _job_ called `pages` on the `master` branch `only`: - -```yaml -image: ruby:2.3 - -pages: - script: - - bundle install - - bundle exec jekyll build -d public - artifacts: - paths: - - public - only: - - master -``` - -### Stages - -Another interesting concept to keep in mind are build stages. -Your web app can pass through a lot of tests and other tasks -until it's deployed to staging or production environments. -There are three default stages on GitLab CI: build, test, -and deploy. To specify which stage your _job_ is running, -simply add another line to your CI: - -```yaml -image: ruby:2.3 - -pages: - stage: deploy - script: - - bundle install - - bundle exec jekyll build -d public - artifacts: - paths: - - public - only: - - master -``` - -You might ask yourself: "why should I bother with stages -at all?" Well, let's say you want to be able to test your -script and check the built site before deploying your site -to production. You want to run the test exactly as your -script will do when you push to `master`. It's simple, -let's add another task (_job_) to our CI, telling it to -test every push to other branches, `except` the `master` branch: - -```yaml -image: ruby:2.3 - -pages: - stage: deploy - script: - - bundle install - - bundle exec jekyll build -d public - artifacts: - paths: - - public - only: - - master - -test: - stage: test - script: - - bundle install - - bundle exec jekyll build -d test - artifacts: - paths: - - test - except: - - master -``` - -The `test` job is running on the stage `test`, Jekyll -will build the site in a directory called `test`, and -this job will affect all the branches except `master`. - -The best benefit of applying _stages_ to different -_jobs_ is that every job in the same stage builds in -parallel. So, if your web app needs more than one test -before being deployed, you can run all your test at the -same time, it's not necessary to wait one test to finish -to run the other. Of course, this is just a brief -introduction of GitLab CI and GitLab Runner, which are -tools much more powerful than that. This is what you -need to be able to create and tweak your builds for -your GitLab Pages site. - -### Before Script - -To avoid running the same script multiple times across -your _jobs_, you can add the parameter `before_script`, -in which you specify which commands you want to run for -every single _job_. In our example, notice that we run -`bundle install` for both jobs, `pages` and `test`. -We don't need to repeat it: - -```yaml -image: ruby:2.3 - -before_script: - - bundle install - -pages: - stage: deploy - script: - - bundle exec jekyll build -d public - artifacts: - paths: - - public - only: - - master - -test: - stage: test - script: - - bundle exec jekyll build -d test - artifacts: - paths: - - test - except: - - master -``` - -### Caching Dependencies - -If you want to cache the installation files for your -projects dependencies, for building faster, you can -use the parameter `cache`. For this example, we'll -cache Jekyll dependencies in a `vendor` directory -when we run `bundle install`: - -```yaml -image: ruby:2.3 - -cache: - paths: - - vendor/ - -before_script: - - bundle install --path vendor - -pages: - stage: deploy - script: - - bundle exec jekyll build -d public - artifacts: - paths: - - public - only: - - master - -test: - stage: test - script: - - bundle exec jekyll build -d test - artifacts: - paths: - - test - except: - - master -``` - -For this specific case, we need to exclude `/vendor` -from Jekyll `_config.yml` file, otherwise Jekyll will -understand it as a regular directory to build -together with the site: - -```yml -exclude: - - vendor -``` - -There we go! Now our GitLab CI not only builds our website, -but also **continuously test** pushes to feature-branches, -**caches** dependencies installed with Bundler, and -**continuously deploy** every push to the `master` branch. - -## Advanced GitLab CI for GitLab Pages - -What you can do with GitLab CI is pretty much up to your -creativity. Once you get used to it, you start creating -awesome scripts that automate most of tasks you'd do -manually in the past. Read through the -[documentation of GitLab CI](https://docs.gitlab.com/ce/ci/yaml/README.html) -to understand how to go even further on your scripts. - -- On this blog post, understand the concept of -[using GitLab CI `environments` to deploy your -web app to staging and production](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/). -- On this post, learn [how to run jobs sequentially, -in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/) -- On this blog post, we go through the process of -[pulling specific directories from different projects](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/) -to deploy this website you're looking at, docs.gitlab.com. -- On this blog post, we teach you [how to use GitLab Pages to produce a code coverage report](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/). - -||| -|:--|--:| -|[**← Part 2: Quick start guide - Setting up GitLab Pages**](getting_started_part_two.md)|| +This document was moved to [another location](../user/project/pages/getting_started_part_three.md). diff --git a/doc/pages/getting_started_part_two.md b/doc/pages/getting_started_part_two.md index 07dd24122c4..a58affec73d 100644 --- a/doc/pages/getting_started_part_two.md +++ b/doc/pages/getting_started_part_two.md @@ -1,152 +1 @@ -# GitLab Pages from A to Z: Part 2 - -> Type: user guide -> -> Level: beginner - -- _[Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](getting_started_part_one.md)_ -- **Part 2: Quick Start Guide - Setting Up GitLab Pages** -- _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_three.md)_ - ----- - -## Setting up GitLab Pages - -For a complete step-by-step tutorial, please read the -blog post [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/). The following sections will explain -what do you need and why do you need them. - -## What you need to get started - -1. A project -1. A configuration file (`.gitlab-ci.yml`) to deploy your site -1. A specific `job` called `pages` in the configuration file -that will make GitLab aware that you are deploying a GitLab Pages website - -Optional Features: - -1. A custom domain or subdomain -1. A DNS pointing your (sub)domain to your Pages site - 1. **Optional**: an SSL/TLS certificate so your custom - domain is accessible under HTTPS. - -## Project - -Your GitLab Pages project is a regular project created the -same way you do for the other ones. To get started with GitLab Pages, you have two ways: - -- Fork one of the templates from Page Examples, or -- Create a new project from scratch - -Let's go over both options. - -### Fork a project to get started from - -To make things easy for you, we've created this -[group](https://gitlab.com/pages) of default projects -containing the most popular SSGs templates. - -Watch the [video tutorial](https://youtu.be/TWqh9MtT4Bg) we've -created for the steps below. - -1. Choose your SSG template -1. Fork a project from the [Pages group](https://gitlab.com/pages) -1. Remove the fork relationship by navigating to your **Project**'s **Settings** > **Edit Project** - - ![remove fork relashionship](img/remove_fork_relashionship.png) - -1. Enable Shared Runners for your fork: navigate to your **Project**'s **Settings** > **CI/CD Pipelines** -1. Trigger a build (push a change to any file) -1. As soon as the build passes, your website will have been deployed with GitLab Pages. Your website URL will be available under your **Project**'s **Settings** > **Pages** - -To turn a **project website** forked from the Pages group into a **user/group** website, you'll need to: - -- Rename it to `namespace.gitlab.io`: navigate to **Project**'s **Settings** > **Edit Project** > **Rename repository** -- Adjust your SSG's [base URL](#urls-and-baseurls) to from `"project-name"` to `""`. This setting will be at a different place for each SSG, as each of them have their own structure and file tree. Most likelly, it will be in the SSG's config file. - -> **Notes:** -> ->1. Why do I need to remove the fork relationship? -> -> Unless you want to contribute to the original project, -you won't need it connected to the upstream. A -[fork](https://about.gitlab.com/2016/12/01/how-to-keep-your-fork-up-to-date-with-its-origin/#fork) -is useful for submitting merge requests to the upstream. -> -> 2. Why do I need to enable Shared Runners? -> -> Shared Runners will run the script set by your GitLab CI -configuration file. They're enabled by default to new projects, -but not to forks. - -### Create a project from scratch - -1. From your **Project**'s **[Dashboard](https://gitlab.com/dashboard/projects)**, -click **New project**, and name it considering the -[practical examples](getting_started_part_one.md#practical-examples). -1. Clone it to your local computer, add your website -files to your project, add, commit and push to GitLab. -1. From the your **Project**'s page, click **Set up CI**: - - ![setup GitLab CI](img/setup_ci.png) - -1. Choose one of the templates from the dropbox menu. -Pick up the template corresponding to the SSG you're using (or plain HTML). - - ![gitlab-ci templates](img/choose_ci_template.png) - -Once you have both site files and `.gitlab-ci.yml` in your project's -root, GitLab CI will build your site and deploy it with Pages. -Once the first build passes, you see your site is live by -navigating to your **Project**'s **Settings** > **Pages**, -where you'll find its default URL. - -> **Notes:** -> -> - GitLab Pages [supports any SSG](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/), but, -if you don't find yours among the templates, you'll need -to configure your own `.gitlab-ci.yml`. Do do that, please -read through the article [Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_three.md). New SSGs are very welcome among -the [example projects](https://gitlab.com/pages). If you set -up a new one, please -[contribute](https://gitlab.com/pages/pages.gitlab.io/blob/master/CONTRIBUTING.md) -to our examples. -> -> - The second step _"Clone it to your local computer"_, can be done -differently, achieving the same results: instead of cloning the bare -repository to you local computer and moving your site files into it, -you can run `git init` in your local website directory, add the -remote URL: `git remote add origin git@gitlab.com:namespace/project-name.git`, -then add, commit, and push. - -### URLs and Baseurls - -Every Static Site Generator (SSG) default configuration expects -to find your website under a (sub)domain (`example.com`), not -in a subdirectory of that domain (`example.com/subdir`). Therefore, -whenever you publish a project website (`namespace.gitlab.io/project-name`), -you'll have to look for this configuration (base URL) on your SSG's -documentation and set it up to reflect this pattern. - -For example, for a Jekyll site, the `baseurl` is defined in the Jekyll -configuration file, `_config.yml`. If your website URL is -`https://john.gitlab.io/blog/`, you need to add this line to `_config.yml`: - -```yaml -baseurl: "/blog" -``` - -On the contrary, if you deploy your website after forking one of -our [default examples](https://gitlab.com/pages), the baseurl will -already be configured this way, as all examples there are project -websites. If you decide to make yours a user or group website, you'll -have to remove this configuration from your project. For the Jekyll -example we've just mentioned, you'd have to change Jekyll's `_config.yml` to: - -```yaml -baseurl: "" -``` - -||| -|:--|--:| -|[**← Part 1: Static sites, domains, DNS records, and SSL/TLS certificates**](getting_started_part_one.md)|[**Part 3: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages →**](getting_started_part_three.md)| +This document was moved to [another location](../user/project/pages/getting_started_part_two.md). diff --git a/doc/pages/img/dns_cname_record_example.png b/doc/pages/img/dns_cname_record_example.png Binary files differdeleted file mode 100644 index d64a843a283..00000000000 --- a/doc/pages/img/dns_cname_record_example.png +++ /dev/null diff --git a/doc/pages/img/remove_fork_relashionship.png b/doc/pages/img/remove_fork_relashionship.png Binary files differdeleted file mode 100644 index f5b5e543f21..00000000000 --- a/doc/pages/img/remove_fork_relashionship.png +++ /dev/null diff --git a/doc/pages/img/setup_ci.png b/doc/pages/img/setup_ci.png Binary files differdeleted file mode 100644 index 7ce0431f4d4..00000000000 --- a/doc/pages/img/setup_ci.png +++ /dev/null diff --git a/doc/pages/index.md b/doc/pages/index.md deleted file mode 100644 index a6f928cc243..00000000000 --- a/doc/pages/index.md +++ /dev/null @@ -1,49 +0,0 @@ -# All you need to know about GitLab Pages - -With GitLab Pages you can create static websites for your GitLab projects, -groups, or user accounts. You can use any static website generator: Jekyll, -Middleman, Hexo, Hugo, Pelican, you name it! Connect as many customs domains -as you like and bring your own TLS certificate to secure them. - -Here's some info we have gathered to get you started. - -## General info - -- [Product webpage](https://pages.gitlab.io) -- [We're bringing GitLab Pages to CE](https://about.gitlab.com/2016/12/24/were-bringing-gitlab-pages-to-community-edition/) -- [Pages group - templates](https://gitlab.com/pages) - -## Getting started - -- GitLab Pages from A to Z - - [Part 1: Static sites, domains, DNS records, and SSL/TLS certificates](getting_started_part_one.md) - - [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md) - - [Part 3: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_three.md) -- [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) a comprehensive step-by-step guide -- Secure GitLab Pages custom domain with SSL/TLS certificates - - [Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/) - - [CloudFlare](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) - - [StartSSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/) -- Static Site Generators - Blog posts series - - [SSGs part 1: Static vs dynamic websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/) - - [SSGs part 2: Modern static site generators](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/) - - [SSGs part 3: Build any SSG site with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) -- [Posting to your GitLab Pages blog from iOS](https://about.gitlab.com/2016/08/19/posting-to-your-gitlab-pages-blog-from-ios/) - -## Video tutorials - -- [How to publish a website with GitLab Pages on GitLab.com: from a forked project](https://youtu.be/TWqh9MtT4Bg) -- [How to Enable GitLab Pages for GitLab CE and EE](https://youtu.be/dD8c7WNcc6s) - -## Advanced use - -- Blog Posts: - - [GitLab CI: Run jobs sequentially, in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/) - - [GitLab CI: Deployment & environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/) - - [Building a new GitLab docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/) - - [Publish code coverage reports with GitLab Pages](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/) - -## Specific documentation - -- [User docs](../user/project/pages/index.md) -- [Admin docs](../administration/pages/index.md) diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index a5b8cd6455c..96ec1b178b6 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -38,23 +38,6 @@ If you are running GitLab within a Docker container, you can run the backup from docker exec -t <container name> gitlab-rake gitlab:backup:create ``` -You can specify that portions of the application data be skipped using the -environment variable `SKIP`. You can skip: - -- `db` (database) -- `uploads` (attachments) -- `repositories` (Git repositories data) -- `builds` (CI job output logs) -- `artifacts` (CI job artifacts) -- `lfs` (LFS objects) -- `registry` (Container Registry images) - -Separate multiple data types to skip using a comma. For example: - -``` -sudo gitlab-rake gitlab:backup:create SKIP=db,uploads -``` - Example output: ``` @@ -111,13 +94,14 @@ To use the `copy` strategy instead of the default streaming strategy, specify You can choose what should be backed up by adding the environment variable `SKIP`. The available options are: -* `db` -* `uploads` (attachments) -* `repositories` -* `builds` (CI build output logs) -* `artifacts` (CI build artifacts) -* `lfs` (LFS objects) -* `pages` (pages content) +- `db` (database) +- `uploads` (attachments) +- `repositories` (Git repositories data) +- `builds` (CI job output logs) +- `artifacts` (CI job artifacts) +- `lfs` (LFS objects) +- `registry` (Container Registry images) +- `pages` (Pages content) Use a comma to specify several options at the same time: @@ -416,7 +400,7 @@ sudo gitlab-rake gitlab:check SANITIZE=true If there is a GitLab version mismatch between your backup tar file and the installed version of GitLab, the restore command will abort with an error. Install the -[correct GitLab version](https://www.gitlab.com/downloads/archives/) and try again. +[correct GitLab version](https://about.gitlab.com/downloads/archives/) and try again. ## Configure cron to make daily backups diff --git a/doc/ssh/README.md b/doc/ssh/README.md index 9e391d647a8..678f5199b02 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -13,7 +13,7 @@ read [this nice tutorial by DigitalOcean](https://www.digitalocean.com/community ## Locating an existing SSH key pair -Before generating a new SSH key check if your system already has one +Before generating a new SSH key pair check if your system already has one at the default location by opening a shell, or Command Prompt on Windows, and running the following command: @@ -23,43 +23,49 @@ and running the following command: type %userprofile%\.ssh\id_rsa.pub ``` -**GNU/Linux / macOS / PowerShell:** +**Git Bash on Windows / GNU/Linux / macOS / PowerShell:** ```bash cat ~/.ssh/id_rsa.pub ``` If you see a string starting with `ssh-rsa` you already have an SSH key pair -and you can skip the next step **Generating a new SSH key pair** -and continue onto **Copying your public SSH key to the clipboard**. +and you can skip the generate portion of the next section and skip to the copy +to clipboard step. If you don't see the string or would like to generate a SSH key pair with a custom name continue onto the next step. +> +**Note:** Public SSH key may also be named as follows: +- `id_dsa.pub` +- `id_ecdsa.pub` +- `id_ed25519.pub` + ## Generating a new SSH key pair -1. To generate a new SSH key, use the following command: +1. To generate a new SSH key pair, use the following command: - **GNU/Linux / macOS:** + **Git Bash on Windows / GNU/Linux / macOS:** ```bash - ssh-keygen -t rsa -C "GitLab" -b 4096 + ssh-keygen -t rsa -C "your.email@example.com" -b 4096 ``` **Windows:** - On Windows you will need to download + Alternatively on Windows you can download [PuttyGen](http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html) - and follow this [documentation article][winputty] to generate a SSH key pair. + and follow [this documentation article][winputty] to generate a SSH key pair. -1. Next, you will be prompted to input a file path to save your key pair to. +1. Next, you will be prompted to input a file path to save your SSH key pair to. If you don't already have an SSH key pair use the suggested path by pressing - enter. Using the suggested path will allow your SSH client - to automatically use the key pair with no additional configuration. + enter. Using the suggested path will normally allow your SSH client + to automatically use the SSH key pair with no additional configuration. - If you already have a key pair with the suggested file path, you will need - to input a new file path and declare what host this key pair will be used - for in your `.ssh/config` file, see **Working with non-default SSH key pair paths** + If you already have a SSH key pair with the suggested file path, you will need + to input a new file path and declare what host this SSH key pair will be used + for in your `.ssh/config` file, see [**Working with non-default SSH key pair paths**](#working-with-non-default-ssh-key-pair-paths) for more information. 1. Once you have input a file path you will be prompted to input a password to @@ -68,12 +74,12 @@ custom name continue onto the next step. pressing enter. >**Note:** - If you want to change the password of your key, you can use `ssh-keygen -p <keyname>`. + If you want to change the password of your SSH key pair, you can use + `ssh-keygen -p <keyname>`. -1. The next step is to copy the public key as we will need it afterwards. +1. The next step is to copy the public SSH key as we will need it afterwards. - To copy your public key to the clipboard, use the appropriate code for your - operating system below: + To copy your public SSH key to the clipboard, use the appropriate code below: **macOS:** @@ -93,7 +99,7 @@ custom name continue onto the next step. type %userprofile%\.ssh\id_rsa.pub | clip ``` - **Windows PowerShell:** + **Git Bash on Windows / Windows PowerShell:** ```bash cat ~/.ssh/id_rsa.pub | clip @@ -101,22 +107,38 @@ custom name continue onto the next step. 1. The final step is to add your public SSH key to GitLab. - Navigate to the 'SSH Keys' tab in you 'Profile Settings'. + Navigate to the 'SSH Keys' tab in your 'Profile Settings'. Paste your key in the 'Key' section and give it a relevant 'Title'. Use an identifiable title like 'Work Laptop - Windows 7' or 'Home MacBook Pro 15'. If you manually copied your public SSH key make sure you copied the entire key starting with `ssh-rsa` and ending with your email. + +1. Optionally you can test your setup by running `ssh -T git@example.com` + (replacing `example.com` with your GitLab domain) and verifying that you + receive a `Welcome to GitLab` message. ## Working with non-default SSH key pair paths If you used a non-default file path for your GitLab SSH key pair, -you must configure your SSH client to find your GitLab SSH private key -for connections to your GitLab server (perhaps gitlab.com). +you must configure your SSH client to find your GitLab private SSH key +for connections to your GitLab server (perhaps `gitlab.com`). + +For your current terminal session you can do so using the following commands +(replacing `other_id_rsa` with your private SSH key): -For OpenSSH clients this is configured in the `~/.ssh/config` file. -Below are two example host configurations using their own key: +**Git Bash on Windows / GNU/Linux / macOS:** + +```bash +eval $(ssh-agent -s) +ssh-add ~/.ssh/other_id_rsa +``` + +To retain these settings you'll need to save them to a configuration file. +For OpenSSH clients this is configured in the `~/.ssh/config` file for some +operating systems. +Below are two example host configurations using their own SSH key: ``` # GitLab.com server @@ -140,8 +162,8 @@ That's why it needs to uniquely map to a single user. ## Deploy keys -Deploy keys allow read-only access to multiple projects with a single SSH -key. +Deploy keys allow read-only or read-write (if enabled) access to one or +multiple projects with a single SSH key pair. This is really useful for cloning repositories to your Continuous Integration (CI) server. By using deploy keys, you don't have to setup a @@ -150,7 +172,8 @@ dummy user account. If you are a project master or owner, you can add a deploy key in the project settings under the section 'Deploy Keys'. Press the 'New Deploy Key' button and upload a public SSH key. After this, the machine that uses -the corresponding private key has read-only access to the project. +the corresponding private SSH key has read-only or read-write (if enabled) +access to the project. You can't add the same deploy key twice with the 'New Deploy Key' option. If you want to add the same key to another project, please enable it in the @@ -166,6 +189,18 @@ project. ### Eclipse -How to add your ssh key to Eclipse: https://wiki.eclipse.org/EGit/User_Guide#Eclipse_SSH_Configuration +How to add your SSH key to Eclipse: https://wiki.eclipse.org/EGit/User_Guide#Eclipse_SSH_Configuration [winputty]: https://the.earth.li/~sgtatham/putty/0.67/htmldoc/Chapter8.html#pubkey-puttygen + +## Troubleshooting + +If on Git clone you are prompted for a password like `git@gitlab.com's password:` +something is wrong with your SSH setup. + +- Ensure that you generated your SSH key pair correctly and added the public SSH + key to your GitLab profile +- Try manually registering your private SSH key using `ssh-agent` as documented + earlier in this document +- Try to debug the connection by running `ssh -Tv git@example.com` + (replacing `example.com` with your GitLab domain) diff --git a/doc/update/8.17-to-9.0.md b/doc/update/8.17-to-9.0.md new file mode 100644 index 00000000000..7b934ecd87a --- /dev/null +++ b/doc/update/8.17-to-9.0.md @@ -0,0 +1,24 @@ +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +cd /home/git/gitlab + +# For HTTPS configurations +git diff origin/8-17-stable:lib/support/nginx/gitlab-ssl origin/9-0-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/8-17-stable:lib/support/nginx/gitlab origin/9-0-stable:lib/support/nginx/gitlab +``` + +If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx +configuration as GitLab application no longer handles setting it. + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-0-stable/lib/support/init.d/gitlab.default.example#L38 diff --git a/doc/user/project/integrations/kubernetes.md b/doc/user/project/integrations/kubernetes.md index cc67e667472..2a890acde4d 100644 --- a/doc/user/project/integrations/kubernetes.md +++ b/doc/user/project/integrations/kubernetes.md @@ -49,7 +49,8 @@ GitLab CI build environment: - `KUBE_URL` - equal to the API URL - `KUBE_TOKEN` - `KUBE_NAMESPACE` -- `KUBE_CA_PEM` - only if a custom CA bundle was specified +- `KUBE_CA_PEM_FILE` - only present if a custom CA bundle was specified. Path to a file containing PEM data. +- `KUBE_CA_PEM` (deprecated)- only if a custom CA bundle was specified. Raw PEM data. ## Web terminals diff --git a/doc/user/project/merge_requests/img/merge_when_build_succeeds_enable.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_enable.png Binary files differdeleted file mode 100644 index f50a1be24f2..00000000000 --- a/doc/user/project/merge_requests/img/merge_when_build_succeeds_enable.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_settings.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_settings.png Binary files differdeleted file mode 100644 index ddc58ff2630..00000000000 --- a/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_settings.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/merge_when_build_succeeds_status.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_status.png Binary files differdeleted file mode 100644 index a98636ee359..00000000000 --- a/doc/user/project/merge_requests/img/merge_when_build_succeeds_status.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_enable.png b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_enable.png Binary files differnew file mode 100644 index 00000000000..33f5a4a7a02 --- /dev/null +++ b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_enable.png diff --git a/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_msg.png b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_only_if_succeeds_msg.png Binary files differindex c43f76b058c..c43f76b058c 100644 --- a/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_msg.png +++ b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_only_if_succeeds_msg.png diff --git a/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_only_if_succeeds_settings.png b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_only_if_succeeds_settings.png Binary files differnew file mode 100644 index 00000000000..9629ed99838 --- /dev/null +++ b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_only_if_succeeds_settings.png diff --git a/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_status.png b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_status.png Binary files differnew file mode 100644 index 00000000000..d0691437c65 --- /dev/null +++ b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_status.png diff --git a/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md b/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md index c63a408505f..bdd7d0022e6 100644 --- a/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md +++ b/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md @@ -5,7 +5,7 @@ more CI jobs running, you can set it to be merged automatically when the jobs pipeline succeeds. This way, you don't have to wait for the jobs to finish and remember to merge the request manually. -![Enable](img/merge_when_build_succeeds_enable.png) +![Enable](img/merge_when_pipeline_succeeds_enable.png) When you hit the "Merge When Pipeline Succeeds" button, the status of the merge request will be updated to represent the impending merge. If you cannot wait @@ -16,7 +16,7 @@ Both team developers and the author of the merge request have the option to cancel the automatic merge if they find a reason why it shouldn't be merged after all. -![Status](img/merge_when_build_succeeds_status.png) +![Status](img/merge_when_pipeline_succeeds_status.png) When the pipeline succeeds, the merge request will automatically be merged. When the pipeline fails, the author gets a chance to retry any failed jobs, @@ -32,15 +32,16 @@ changes to be reviewed. > **Note:** You need to have jobs configured to enable this feature. -You can prevent merge requests from being merged if their pipeline did not succeed. +You can prevent merge requests from being merged if their pipeline did not succeed +or if there are discussions to be resolved. Navigate to your project's settings page, select the **Only allow merge requests to be merged if the pipeline succeeds** check box and hit **Save** for the changes to take effect. -![Only allow merge if pipeline succeeds settings](img/merge_when_build_succeeds_only_if_succeeds_settings.png) +![Only allow merge if pipeline succeeds settings](img/merge_when_pipeline_succeeds_only_if_succeeds_settings.png) From now on, every time the pipeline fails you will not be able to merge the merge request from the UI, until you make all relevant jobs pass. -![Only allow merge if pipeline succeeds message](img/merge_when_build_succeeds_only_if_succeeds_msg.png) +![Only allow merge if pipeline succeeds message](img/merge_when_pipeline_succeeds_only_if_succeeds_msg.png) diff --git a/doc/user/project/pages/getting_started_part_four.md b/doc/user/project/pages/getting_started_part_four.md new file mode 100644 index 00000000000..6edf99239ea --- /dev/null +++ b/doc/user/project/pages/getting_started_part_four.md @@ -0,0 +1,382 @@ +# GitLab Pages from A to Z: Part 4 + +- [Part 1: Static sites and GitLab Pages domains](getting_started_part_one.md) +- [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md) +- [Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](getting_started_part_three.md) +- **Part 4: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages** + +## Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages + +[GitLab CI](https://about.gitlab.com/gitlab-ci/) serves +numerous purposes, to build, test, and deploy your app +from GitLab through +[Continuous Integration, Continuous Delivery, and Continuous Deployment](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) +methods. You will need it to build your website with GitLab Pages, +and deploy it to the Pages server. + +What this file actually does is telling the +[GitLab Runner](https://docs.gitlab.com/runner/) to run scripts +as you would do from the command line. The Runner acts as your +terminal. GitLab CI tells the Runner which commands to run. +Both are built-in in GitLab, and you don't need to set up +anything for them to work. + +Explaining [every detail of GitLab CI](https://docs.gitlab.com/ce/ci/yaml/README.html) +and GitLab Runner is out of the scope of this guide, but we'll +need to understand just a few things to be able to write our own +`.gitlab-ci.yml` or tweak an existing one. It's an +[Yaml](http://docs.ansible.com/ansible/YAMLSyntax.html) file, +with its own syntax. You can always check your CI syntax with +the [GitLab CI Lint Tool](https://gitlab.com/ci/lint). + +**Practical Example:** + +Let's consider you have a [Jekyll](https://jekyllrb.com/) site. +To build it locally, you would open your terminal, and run `jekyll build`. +Of course, before building it, you had to install Jekyll in your computer. +For that, you had to open your terminal and run `gem install jekyll`. +Right? GitLab CI + GitLab Runner do the same thing. But you need to +write in the `.gitlab-ci.yml` the script you want to run so +GitLab Runner will do it for you. It looks more complicated then it +is. What you need to tell the Runner: + +``` +$ gem install jekyll +$ jekyll build +``` + +### Script + +To transpose this script to Yaml, it would be like this: + +```yaml +script: + - gem install jekyll + - jekyll build +``` + +### Job + +So far so good. Now, each `script`, in GitLab is organized by +a `job`, which is a bunch of scripts and settings you want to +apply to that specific task. + +```yaml +job: + script: + - gem install jekyll + - jekyll build +``` + +For GitLab Pages, this `job` has a specific name, called `pages`, +which tells the Runner you want that task to deploy your website +with GitLab Pages: + +```yaml +pages: + script: + - gem install jekyll + - jekyll build +``` + +### The `public` directory + +We also need to tell Jekyll where do you want the website to build, +and GitLab Pages will only consider files in a directory called `public`. +To do that with Jekyll, we need to add a flag specifying the +[destination (`-d`)](https://jekyllrb.com/docs/usage/) of the +built website: `jekyll build -d public`. Of course, we need +to tell this to our Runner: + +```yaml +pages: + script: + - gem install jekyll + - jekyll build -d public +``` + +### Artifacts + +We also need to tell the Runner that this _job_ generates +_artifacts_, which is the site built by Jekyll. +Where are these artifacts stored? In the `public` directory: + +```yaml +pages: + script: + - gem install jekyll + - jekyll build -d public + artifacts: + paths: + - public +``` + +The script above would be enough to build your Jekyll +site with GitLab Pages. But, from Jekyll 3.4.0 on, its default +template originated by `jekyll new project` requires +[Bundler](http://bundler.io/) to install Jekyll dependencies +and the default theme. To adjust our script to meet these new +requirements, we only need to install and build Jekyll with Bundler: + +```yaml +pages: + script: + - bundle install + - bundle exec jekyll build -d public + artifacts: + paths: + - public +``` + +That's it! A `.gitlab-ci.yml` with the content above would deploy +your Jekyll 3.4.0 site with GitLab Pages. This is the minimum +configuration for our example. On the steps below, we'll refine +the script by adding extra options to our GitLab CI. + +### Image + +At this point, you probably ask yourself: "okay, but to install Jekyll +I need Ruby. Where is Ruby on that script?". The answer is simple: the +first thing GitLab Runner will look for in your `.gitlab-ci.yml` is a +[Docker](https://www.docker.com/) image specifying what do you need in +your container to run that script: + +```yaml +image: ruby:2.3 + +pages: + script: + - bundle install + - bundle exec jekyll build -d public + artifacts: + paths: + - public +``` + +In this case, you're telling the Runner to pull this image, which +contains Ruby 2.3 as part of its file system. When you don't specify +this image in your configuration, the Runner will use a default +image, which is Ruby 2.1. + +If your SSG needs [NodeJS](https://nodejs.org/) to build, you'll +need to specify which image you want to use, and this image should +contain NodeJS as part of its file system. E.g., for a +[Hexo](https://gitlab.com/pages/hexo) site, you can use `image: node:4.2.2`. + +>**Note:** +We're not trying to explain what a Docker image is, +we just need to introduce the concept with a minimum viable +explanation. To know more about Docker images, please visit +their website or take a look at a +[summarized explanation](http://paislee.io/how-to-automate-docker-deployments/) here. + +Let's go a little further. + +### Branching + +If you use GitLab as a version control platform, you will have your +branching strategy to work on your project. Meaning, you will have +other branches in your project, but you'll want only pushes to the +default branch (usually `master`) to be deployed to your website. +To do that, we need to add another line to our CI, telling the Runner +to only perform that _job_ called `pages` on the `master` branch `only`: + +```yaml +image: ruby:2.3 + +pages: + script: + - bundle install + - bundle exec jekyll build -d public + artifacts: + paths: + - public + only: + - master +``` + +### Stages + +Another interesting concept to keep in mind are build stages. +Your web app can pass through a lot of tests and other tasks +until it's deployed to staging or production environments. +There are three default stages on GitLab CI: build, test, +and deploy. To specify which stage your _job_ is running, +simply add another line to your CI: + +```yaml +image: ruby:2.3 + +pages: + stage: deploy + script: + - bundle install + - bundle exec jekyll build -d public + artifacts: + paths: + - public + only: + - master +``` + +You might ask yourself: "why should I bother with stages +at all?" Well, let's say you want to be able to test your +script and check the built site before deploying your site +to production. You want to run the test exactly as your +script will do when you push to `master`. It's simple, +let's add another task (_job_) to our CI, telling it to +test every push to other branches, `except` the `master` branch: + +```yaml +image: ruby:2.3 + +pages: + stage: deploy + script: + - bundle install + - bundle exec jekyll build -d public + artifacts: + paths: + - public + only: + - master + +test: + stage: test + script: + - bundle install + - bundle exec jekyll build -d test + artifacts: + paths: + - test + except: + - master +``` + +The `test` job is running on the stage `test`, Jekyll +will build the site in a directory called `test`, and +this job will affect all the branches except `master`. + +The best benefit of applying _stages_ to different +_jobs_ is that every job in the same stage builds in +parallel. So, if your web app needs more than one test +before being deployed, you can run all your test at the +same time, it's not necessary to wait one test to finish +to run the other. Of course, this is just a brief +introduction of GitLab CI and GitLab Runner, which are +tools much more powerful than that. This is what you +need to be able to create and tweak your builds for +your GitLab Pages site. + +### Before Script + +To avoid running the same script multiple times across +your _jobs_, you can add the parameter `before_script`, +in which you specify which commands you want to run for +every single _job_. In our example, notice that we run +`bundle install` for both jobs, `pages` and `test`. +We don't need to repeat it: + +```yaml +image: ruby:2.3 + +before_script: + - bundle install + +pages: + stage: deploy + script: + - bundle exec jekyll build -d public + artifacts: + paths: + - public + only: + - master + +test: + stage: test + script: + - bundle exec jekyll build -d test + artifacts: + paths: + - test + except: + - master +``` + +### Caching Dependencies + +If you want to cache the installation files for your +projects dependencies, for building faster, you can +use the parameter `cache`. For this example, we'll +cache Jekyll dependencies in a `vendor` directory +when we run `bundle install`: + +```yaml +image: ruby:2.3 + +cache: + paths: + - vendor/ + +before_script: + - bundle install --path vendor + +pages: + stage: deploy + script: + - bundle exec jekyll build -d public + artifacts: + paths: + - public + only: + - master + +test: + stage: test + script: + - bundle exec jekyll build -d test + artifacts: + paths: + - test + except: + - master +``` + +For this specific case, we need to exclude `/vendor` +from Jekyll `_config.yml` file, otherwise Jekyll will +understand it as a regular directory to build +together with the site: + +```yml +exclude: + - vendor +``` + +There we go! Now our GitLab CI not only builds our website, +but also **continuously test** pushes to feature-branches, +**caches** dependencies installed with Bundler, and +**continuously deploy** every push to the `master` branch. + +## Advanced GitLab CI for GitLab Pages + +What you can do with GitLab CI is pretty much up to your +creativity. Once you get used to it, you start creating +awesome scripts that automate most of tasks you'd do +manually in the past. Read through the +[documentation of GitLab CI](https://docs.gitlab.com/ce/ci/yaml/README.html) +to understand how to go even further on your scripts. + +- On this blog post, understand the concept of +[using GitLab CI `environments` to deploy your +web app to staging and production](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/). +- On this post, learn [how to run jobs sequentially, +in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/) +- On this blog post, we go through the process of +[pulling specific directories from different projects](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/) +to deploy this website you're looking at, docs.gitlab.com. +- On this blog post, we teach you [how to use GitLab Pages to produce a code coverage report](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/). + +||| +|:--|--:| +|[**← Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates**](getting_started_part_three.md)|| diff --git a/doc/user/project/pages/getting_started_part_one.md b/doc/user/project/pages/getting_started_part_one.md new file mode 100644 index 00000000000..582a4afbab4 --- /dev/null +++ b/doc/user/project/pages/getting_started_part_one.md @@ -0,0 +1,106 @@ +# GitLab Pages from A to Z: Part 1 + +- **Part 1: Static sites and GitLab Pages domains** +- [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md) +- [Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](getting_started_part_three.md) +- [Part 4: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_four.md) + +## GitLab Pages form A to Z + +This is a comprehensive guide, made for those who want to +publish a website with GitLab Pages but aren't familiar with +the entire process involved. + +This [first part](#what-you-need-to-know-before-getting-started) of this series will present you to the concepts of +static sites, and go over how the default Pages domains work. + +The [second part](getting_started_part_two.md) covers how to get started with GitLab Pages: deploy +a website from a forked project or create a new one from scratch. + +The [third part](getting_started_part_three.md) will show you how to set up a custom domain or subdomain +to your site already deployed. + +The [fourth part](getting_started_part_four.md) will show you how to create and tweak GitLab CI for +GitLab Pages. + +To **enable** GitLab Pages for GitLab CE (Community Edition) +and GitLab EE (Enterprise Edition), please read the +[admin documentation](https://docs.gitlab.com/ce/administration/pages/index.html), +and/or watch this [video tutorial](https://youtu.be/dD8c7WNcc6s). + +>**Note:** +For this guide, we assume you already have GitLab Pages +server up and running for your GitLab instance. + +## What you need to know before getting started + +Before we begin, let's understand a few concepts first. + +### Static sites + +GitLab Pages only supports static websites, meaning, +your output files must be HTML, CSS, and JavaScript only. + +To create your static site, you can either hardcode in HTML, +CSS, and JS, or use a [Static Site Generator (SSG)](https://www.staticgen.com/) +to simplify your code and build the static site for you, +which is highly recommendable and much faster than hardcoding. + +#### Further Reading + +- Read through this technical overview on [Static versus Dynamic Websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/) +- Understand [how modern Static Site Generators work](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/) and what you can add to your static site +- You can use [any SSG with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) +- Fork an [example project](https://gitlab.com/pages) to build your website based upon + +### GitLab Pages domain + +If you set up a GitLab Pages project on GitLab.com, +it will automatically be accessible under a +[subdomain of `namespace.pages.io`](https://docs.gitlab.com/ce/user/project/pages/). +The `namespace` is defined by your username on GitLab.com, +or the group name you created this project under. + +>**Note:** +If you use your own GitLab instance to deploy your +site with GitLab Pages, check with your sysadmin what's your +Pages wildcard domain. This guide is valid for any GitLab instance, +you just need to replace Pages wildcard domain on GitLab.com +(`*.gitlab.io`) with your own. + +#### Practical examples + +**Project Websites:** + +- You created a project called `blog` under your username `john`, +therefore your project URL is `https://gitlab.com/john/blog/`. +Once you enable GitLab Pages for this project, and build your site, +it will be available under `https://john.gitlab.io/blog/`. +- You created a group for all your websites called `websites`, +and a project within this group is called `blog`. Your project +URL is `https://gitlab.com/websites/blog/`. Once you enable +GitLab Pages for this project, the site will live under +`https://websites.gitlab.io/blog/`. + +**User and Group Websites:** + +- Under your username, `john`, you created a project called +`john.gitlab.io`. Your project URL will be `https://gitlab.com/john/john.gitlab.io`. +Once you enable GitLab Pages for your project, your website +will be published under `https://john.gitlab.io`. +- Under your group `websites`, you created a project called +`websites.gitlab.io`. your project's URL will be `https://gitlab.com/websites/websites.gitlab.io`. Once you enable GitLab Pages for your project, +your website will be published under `https://websites.gitlab.io`. + +**General example:** + +- On GitLab.com, a project site will always be available under +`https://namespace.gitlab.io/project-name` +- On GitLab.com, a user or group website will be available under +`https://namespace.gitlab.io/` +- On your GitLab instance, replace `gitlab.io` above with your +Pages server domain. Ask your sysadmin for this information. + +||| +|:--|--:| +||[**Part 2: Quick start guide - Setting up GitLab Pages →**](getting_started_part_two.md)| diff --git a/doc/user/project/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md new file mode 100644 index 00000000000..dba5fb6c17a --- /dev/null +++ b/doc/user/project/pages/getting_started_part_three.md @@ -0,0 +1,189 @@ +# GitLab Pages from A to Z: Part 3 + +- [Part 1: Static sites and GitLab Pages domains](getting_started_part_one.md) +- [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md) +- **Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates** +- [Part 4: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_four.md) + +## Setting Up Custom Domains - DNS Records and SSL/TLS Certificates + +As described in the previous part of this series, setting up GitLab Pages with custom domains, and adding SSL/TLS certificates to them, are optional features of GitLab Pages. + +These steps assume you've already [set your site up](getting_started_part_two.md) and and it's served under the default Pages domain `namespace.gitlab.io`, or `namespace.gitlab.io/project-name`. + +### DNS Records + +A Domain Name System (DNS) web service routes visitors to websites +by translating domain names (such as `www.example.com`) into the +numeric IP addresses (such as `192.0.2.1`) that computers use to +connect to each other. + +A DNS record is created to point a (sub)domain to a certain location, +which can be an IP address or another domain. In case you want to use +GitLab Pages with your own (sub)domain, you need to access your domain's +registrar control panel to add a DNS record pointing it back to your +GitLab Pages site. + +Note that **how to** add DNS records depends on which server your domain +is hosted on. Every control panel has its own place to do it. If you are +not an admin of your domain, and don't have access to your registrar, +you'll need to ask for the technical support of your hosting service +to do it for you. + +To help you out, we've gathered some instructions on how to do that +for the most popular hosting services: + +- [Amazon](http://docs.aws.amazon.com/gettingstarted/latest/swh/getting-started-configure-route53.html) +- [Bluehost](https://my.bluehost.com/cgi/help/559) +- [CloudFlare](https://support.cloudflare.com/hc/en-us/articles/200169096-How-do-I-add-A-records-) +- [cPanel](https://documentation.cpanel.net/display/ALD/Edit+DNS+Zone) +- [DreamHost](https://help.dreamhost.com/hc/en-us/articles/215414867-How-do-I-add-custom-DNS-records-) +- [Go Daddy](https://www.godaddy.com/help/add-an-a-record-19238) +- [Hostgator](http://support.hostgator.com/articles/changing-dns-records) +- [Inmotion hosting](https://my.bluehost.com/cgi/help/559) +- [Media Temple](https://mediatemple.net/community/products/dv/204403794/how-can-i-change-the-dns-records-for-my-domain) +- [Microsoft](https://msdn.microsoft.com/en-us/library/bb727018.aspx) + +If your hosting service is not listed above, you can just try to +search the web for "how to add dns record on <my hosting service>". + +#### DNS A record + +In case you want to point a root domain (`example.com`) to your +GitLab Pages site, deployed to `namespace.gitlab.io`, you need to +log into your domain's admin control panel and add a DNS `A` record +pointing your domain to Pages' server IP address. For projects on +GitLab.com, this IP is `104.208.235.32`. For projects leaving in +other GitLab instances (CE or EE), please contact your sysadmin +asking for this information (which IP address is Pages server +running on your instance). + +**Practical Example:** + +![DNS A record pointing to GitLab.com Pages server](img/dns_a_record_example.png) + +#### DNS CNAME record + +In case you want to point a subdomain (`hello-world.example.com`) +to your GitLab Pages site initially deployed to `namespace.gitlab.io`, +you need to log into your domain's admin control panel and add a DNS +`CNAME` record pointing your subdomain to your website URL +(`namespace.gitlab.io`) address. + +Notice that, despite it's a user or project website, the `CNAME` +should point to your Pages domain (`namespace.gitlab.io`), +without any `/project-name`. + +**Practical Example:** + +![DNS CNAME record pointing to GitLab.com project](img/dns_cname_record_example.png) + +#### TL;DR + +| From | DNS Record | To | +| ---- | ---------- | -- | +| domain.com | A | 104.208.235.32 | +| subdomain.domain.com | CNAME | namespace.gitlab.io | + +> **Notes**: +> +> - **Do not** use a CNAME record if you want to point your +`domain.com` to your GitLab Pages site. Use an `A` record instead. +> - **Do not** add any special chars after the default Pages +domain. E.g., **do not** point your `subdomain.domain.com` to +`namespace.gitlab.io.` or `namespace.gitlab.io/`. + +### SSL/TLS Certificates + +Every GitLab Pages project on GitLab.com will be available under +HTTPS for the default Pages domain (`*.gitlab.io`). Once you set +up your Pages project with your custom (sub)domain, if you want +it secured by HTTPS, you will have to issue a certificate for that +(sub)domain and install it on your project. + +>**Note:** +Certificates are NOT required to add to your custom +(sub)domain on your GitLab Pages project, though they are +highly recommendable. + +The importance of having any website securely served under HTTPS +is explained on the introductory section of the blog post +[Secure GitLab Pages with StartSSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/#https-a-quick-overview). + +The reason why certificates are so important is that they encrypt +the connection between the **client** (you, me, your visitors) +and the **server** (where you site lives), through a keychain of +authentications and validations. + +### Issuing Certificates + +GitLab Pages accepts [PEM](https://support.quovadisglobal.com/kb/a37/what-is-pem-format.aspx) certificates issued by +[Certificate Authorities (CA)](https://en.wikipedia.org/wiki/Certificate_authority) +and self-signed certificates. Of course, +[you'd rather issue a certificate than generate a self-signed](https://en.wikipedia.org/wiki/Self-signed_certificate), +for security reasons and for having browsers trusting your +site's certificate. + +There are several different kinds of certificates, each one +with certain security level. A static personal website will +not require the same security level as an online banking web app, +for instance. There are a couple Certificate Authorities that +offer free certificates, aiming to make the internet more secure +to everyone. The most popular is [Let's Encrypt](https://letsencrypt.org/), +which issues certificates trusted by most of browsers, it's open +source, and free to use. Please read through this tutorial to +understand [how to secure your GitLab Pages website with Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/). + +With the same popularity, there are [certificates issued by CloudFlare](https://www.cloudflare.com/ssl/), +which also offers a [free CDN service](https://blog.cloudflare.com/cloudflares-free-cdn-and-you/). +Their certs are valid up to 15 years. Read through the tutorial on +[how to add a CloudFlare Certificate to your GitLab Pages website](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/). + +### Adding certificates to your project + +Regardless the CA you choose, the steps to add your certificate to +your Pages project are the same. + +#### What do you need + +1. A PEM certificate +1. An intermediate certificate +1. A public key + +![Pages project - adding certificates](img/add_certificate_to_pages.png) + +These fields are found under your **Project**'s **Settings** > **Pages** > **New Domain**. + +#### What's what? + +- A PEM certificate is the certificate generated by the CA, +which needs to be added to the field **Certificate (PEM)**. +- An [intermediate certificate](https://en.wikipedia.org/wiki/Intermediate_certificate_authority) (aka "root certificate") is +the part of the encryption keychain that identifies the CA. +Usually it's combined with the PEM certificate, but there are +some cases in which you need to add them manually. +[CloudFlare certs](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) +are one of these cases. +- A public key is an encrypted key which validates +your PEM against your domain. + +#### Now what? + +Now that you hopefully understand why you need all +of this, it's simple: + +- Your PEM certificate needs to be added to the first field +- If your certificate is missing its intermediate, copy +and paste the root certificate (usually available from your CA website) +and paste it in the [same field as your PEM certificate](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/), +just jumping a line between them. +- Copy your public key and paste it in the last field + +>**Note:** +**Do not** open certificates or encryption keys in +regular text editors. Always use code editors (such as +Sublime Text, Atom, Dreamweaver, Brackets, etc). + +||| +|:--|--:| +|[**← Part 2: Quick start guide - Setting up GitLab Pages**](getting_started_part_two.md)|[**Part 4: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages →**](getting_started_part_four.md)| diff --git a/doc/user/project/pages/getting_started_part_two.md b/doc/user/project/pages/getting_started_part_two.md new file mode 100644 index 00000000000..d0e2c467fee --- /dev/null +++ b/doc/user/project/pages/getting_started_part_two.md @@ -0,0 +1,154 @@ +# GitLab Pages from A to Z: Part 2 + +- [Part 1: Static sites and GitLab Pages domains](getting_started_part_one.md) +- **Part 2: Quick start guide - Setting up GitLab Pages** +- [Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](getting_started_part_three.md) +- [Part 4: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_four.md) + +## Setting up GitLab Pages + +For a complete step-by-step tutorial, please read the +blog post [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/). The following sections will explain +what do you need and why do you need them. + +## What you need to get started + +1. A project +1. A configuration file (`.gitlab-ci.yml`) to deploy your site +1. A specific `job` called `pages` in the configuration file +that will make GitLab aware that you are deploying a GitLab Pages website + +Optional Features: + +1. A custom domain or subdomain +1. A DNS pointing your (sub)domain to your Pages site + 1. **Optional**: an SSL/TLS certificate so your custom + domain is accessible under HTTPS. + +The optional settings, custom domain, DNS records, and SSL/TLS certificates, are described in [Part 3](getting_started_part_three.md)). + +## Project + +Your GitLab Pages project is a regular project created the +same way you do for the other ones. To get started with GitLab Pages, you have two ways: + +- Fork one of the templates from Page Examples, or +- Create a new project from scratch + +Let's go over both options. + +### Fork a project to get started from + +To make things easy for you, we've created this +[group](https://gitlab.com/pages) of default projects +containing the most popular SSGs templates. + +Watch the [video tutorial](https://youtu.be/TWqh9MtT4Bg) we've +created for the steps below. + +1. Choose your SSG template +1. Fork a project from the [Pages group](https://gitlab.com/pages) +1. Remove the fork relationship by navigating to your **Project**'s **Settings** > **Edit Project** + + ![remove fork relashionship](img/remove_fork_relashionship.png) + +1. Enable Shared Runners for your fork: navigate to your **Project**'s **Settings** > **CI/CD Pipelines** +1. Trigger a build (push a change to any file) +1. As soon as the build passes, your website will have been deployed with GitLab Pages. Your website URL will be available under your **Project**'s **Settings** > **Pages** + +To turn a **project website** forked from the Pages group into a **user/group** website, you'll need to: + +- Rename it to `namespace.gitlab.io`: navigate to **Project**'s **Settings** > **Edit Project** > **Rename repository** +- Adjust your SSG's [base URL](#urls-and-baseurls) to from `"project-name"` to `""`. This setting will be at a different place for each SSG, as each of them have their own structure and file tree. Most likelly, it will be in the SSG's config file. + +> **Notes:** +> +>1. Why do I need to remove the fork relationship? +> +> Unless you want to contribute to the original project, +you won't need it connected to the upstream. A +[fork](https://about.gitlab.com/2016/12/01/how-to-keep-your-fork-up-to-date-with-its-origin/#fork) +is useful for submitting merge requests to the upstream. +> +> 2. Why do I need to enable Shared Runners? +> +> Shared Runners will run the script set by your GitLab CI +configuration file. They're enabled by default to new projects, +but not to forks. + +### Create a project from scratch + +1. From your **Project**'s **[Dashboard](https://gitlab.com/dashboard/projects)**, +click **New project**, and name it considering the +[practical examples](getting_started_part_one.md#practical-examples). +1. Clone it to your local computer, add your website +files to your project, add, commit and push to GitLab. +1. From the your **Project**'s page, click **Set up CI**: + + ![setup GitLab CI](img/setup_ci.png) + +1. Choose one of the templates from the dropbox menu. +Pick up the template corresponding to the SSG you're using (or plain HTML). + + ![gitlab-ci templates](img/choose_ci_template.png) + +Once you have both site files and `.gitlab-ci.yml` in your project's +root, GitLab CI will build your site and deploy it with Pages. +Once the first build passes, you see your site is live by +navigating to your **Project**'s **Settings** > **Pages**, +where you'll find its default URL. + +> **Notes:** +> +> - GitLab Pages [supports any SSG](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/), but, +if you don't find yours among the templates, you'll need +to configure your own `.gitlab-ci.yml`. Do do that, please +read through the article [Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_four.md). New SSGs are very welcome among +the [example projects](https://gitlab.com/pages). If you set +up a new one, please +[contribute](https://gitlab.com/pages/pages.gitlab.io/blob/master/CONTRIBUTING.md) +to our examples. +> +> - The second step _"Clone it to your local computer"_, can be done +differently, achieving the same results: instead of cloning the bare +repository to you local computer and moving your site files into it, +you can run `git init` in your local website directory, add the +remote URL: `git remote add origin git@gitlab.com:namespace/project-name.git`, +then add, commit, and push. + +### URLs and Baseurls + +Every Static Site Generator (SSG) default configuration expects +to find your website under a (sub)domain (`example.com`), not +in a subdirectory of that domain (`example.com/subdir`). Therefore, +whenever you publish a project website (`namespace.gitlab.io/project-name`), +you'll have to look for this configuration (base URL) on your SSG's +documentation and set it up to reflect this pattern. + +For example, for a Jekyll site, the `baseurl` is defined in the Jekyll +configuration file, `_config.yml`. If your website URL is +`https://john.gitlab.io/blog/`, you need to add this line to `_config.yml`: + +```yaml +baseurl: "/blog" +``` + +On the contrary, if you deploy your website after forking one of +our [default examples](https://gitlab.com/pages), the baseurl will +already be configured this way, as all examples there are project +websites. If you decide to make yours a user or group website, you'll +have to remove this configuration from your project. For the Jekyll +example we've just mentioned, you'd have to change Jekyll's `_config.yml` to: + +```yaml +baseurl: "" +``` + +### Custom Domains + +GitLab Pages supports custom domains and subdomains, served under HTTPS or HTTPS. +Please check the [next part](getting_started_part_three.md) of this series for an overview. + +||| +|:--|--:| +|[**← Part 1: Static sites, domains, DNS records, and SSL/TLS certificates**](getting_started_part_one.md)|[**Setting Up Custom Domains - DNS Records and SSL/TLS Certificates →**](getting_started_part_three.md)| diff --git a/doc/pages/img/add_certificate_to_pages.png b/doc/user/project/pages/img/add_certificate_to_pages.png Binary files differindex d92a981dc60..d92a981dc60 100644 --- a/doc/pages/img/add_certificate_to_pages.png +++ b/doc/user/project/pages/img/add_certificate_to_pages.png diff --git a/doc/pages/img/choose_ci_template.png b/doc/user/project/pages/img/choose_ci_template.png Binary files differindex 0697542abc8..0697542abc8 100644 --- a/doc/pages/img/choose_ci_template.png +++ b/doc/user/project/pages/img/choose_ci_template.png diff --git a/doc/pages/img/dns_a_record_example.png b/doc/user/project/pages/img/dns_a_record_example.png Binary files differindex b923730388a..b923730388a 100644 --- a/doc/pages/img/dns_a_record_example.png +++ b/doc/user/project/pages/img/dns_a_record_example.png diff --git a/doc/user/project/pages/img/dns_cname_record_example.png b/doc/user/project/pages/img/dns_cname_record_example.png Binary files differnew file mode 100644 index 00000000000..43d1a838544 --- /dev/null +++ b/doc/user/project/pages/img/dns_cname_record_example.png diff --git a/doc/user/project/pages/img/pages_create_project.png b/doc/user/project/pages/img/pages_create_project.png Binary files differindex a936d8e5dbd..be47f9d2a44 100644 --- a/doc/user/project/pages/img/pages_create_project.png +++ b/doc/user/project/pages/img/pages_create_project.png diff --git a/doc/user/project/pages/img/pages_create_user_page.png b/doc/user/project/pages/img/pages_create_user_page.png Binary files differindex 3f615d3757d..2f1a19ae424 100644 --- a/doc/user/project/pages/img/pages_create_user_page.png +++ b/doc/user/project/pages/img/pages_create_user_page.png diff --git a/doc/user/project/pages/img/pages_dns_details.png b/doc/user/project/pages/img/pages_dns_details.png Binary files differindex 8d34f3b7f38..274e98fde4d 100644 --- a/doc/user/project/pages/img/pages_dns_details.png +++ b/doc/user/project/pages/img/pages_dns_details.png diff --git a/doc/user/project/pages/img/pages_multiple_domains.png b/doc/user/project/pages/img/pages_multiple_domains.png Binary files differindex 2bc7cee07a6..6bc92db6b41 100644 --- a/doc/user/project/pages/img/pages_multiple_domains.png +++ b/doc/user/project/pages/img/pages_multiple_domains.png diff --git a/doc/user/project/pages/img/pages_new_domain_button.png b/doc/user/project/pages/img/pages_new_domain_button.png Binary files differindex c3640133bb2..cd59defa006 100644 --- a/doc/user/project/pages/img/pages_new_domain_button.png +++ b/doc/user/project/pages/img/pages_new_domain_button.png diff --git a/doc/user/project/pages/img/pages_remove.png b/doc/user/project/pages/img/pages_remove.png Binary files differindex adbfb654877..b064310380e 100644 --- a/doc/user/project/pages/img/pages_remove.png +++ b/doc/user/project/pages/img/pages_remove.png diff --git a/doc/user/project/pages/img/pages_upload_cert.png b/doc/user/project/pages/img/pages_upload_cert.png Binary files differindex 06d85ab1971..dc431ea3fef 100644 --- a/doc/user/project/pages/img/pages_upload_cert.png +++ b/doc/user/project/pages/img/pages_upload_cert.png diff --git a/doc/user/project/pages/img/remove_fork_relashionship.png b/doc/user/project/pages/img/remove_fork_relashionship.png Binary files differnew file mode 100644 index 00000000000..67c45491f08 --- /dev/null +++ b/doc/user/project/pages/img/remove_fork_relashionship.png diff --git a/doc/user/project/pages/img/setup_ci.png b/doc/user/project/pages/img/setup_ci.png Binary files differnew file mode 100644 index 00000000000..214c1cc668f --- /dev/null +++ b/doc/user/project/pages/img/setup_ci.png diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md index 276fbd26835..1366756d593 100644 --- a/doc/user/project/pages/index.md +++ b/doc/user/project/pages/index.md @@ -1,449 +1,48 @@ -# GitLab Pages - -> **Notes:** -> - This feature was [introduced][ee-80] in GitLab EE 8.3. -> - Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5. -> - GitLab Pages [were ported][ce-14605] to Community Edition in GitLab 8.17. -> - This document is about the user guide. To learn how to enable GitLab Pages -> across your GitLab instance, visit the [administrator documentation](../../../administration/pages/index.md). - -With GitLab Pages you can host for free your static websites on GitLab. -Combined with the power of [GitLab CI] and the help of [GitLab Runner] you can -deploy static pages for your individual projects, your user or your group. - -Read [GitLab Pages on GitLab.com](#gitlab-pages-on-gitlab-com) for specific -information, if you are using GitLab.com to host your website. - -Read through [All you Need to Know About GitLab Pages][pages-index-guide] for a list of all learning materials we have prepared for GitLab Pages (webpages, articles, guides, blog posts, video tutorials). - -## Getting started with GitLab Pages - -> **Note:** -> In the rest of this document we will assume that the general domain name that -> is used for GitLab Pages is `example.io`. - -In general there are two types of pages one might create: - -- Pages per user (`username.example.io`) or per group (`groupname.example.io`) -- Pages per project (`username.example.io/projectname` or `groupname.example.io/projectname`) - -In GitLab, usernames and groupnames are unique and we often refer to them -as namespaces. There can be only one namespace in a GitLab instance. Below you -can see the connection between the type of GitLab Pages, what the project name -that is created on GitLab looks like and the website URL it will be ultimately -be served on. - -| Type of GitLab Pages | The name of the project created in GitLab | Website URL | -| -------------------- | ------------ | ----------- | -| User pages | `username.example.io` | `http(s)://username.example.io` | -| Group pages | `groupname.example.io` | `http(s)://groupname.example.io` | -| Project pages owned by a user | `projectname` | `http(s)://username.example.io/projectname` | -| Project pages owned by a group | `projectname` | `http(s)://groupname.example.io/projectname`| - -> **Warning:** -> There are some known [limitations](#limitations) regarding namespaces served -> under the general domain name and HTTPS. Make sure to read that section. - -### GitLab Pages requirements - -In brief, this is what you need to upload your website in GitLab Pages: - -1. Find out the general domain name that is used for GitLab Pages - (ask your administrator). This is very important, so you should first make - sure you get that right. -1. Create a project -1. Push a [`.gitlab-ci.yml` file][yaml] in the root directory - of your repository with a specific job named [`pages`][pages] -1. Set up a GitLab Runner to build your website - -> **Note:** -If [shared runners](../../../ci/runners/README.md) are enabled by your GitLab -administrator, you should be able to use them instead of bringing your own. - -### User or group Pages - -For user and group pages, the name of the project should be specific to the -username or groupname and the general domain name that is used for GitLab Pages. -Head over your GitLab instance that supports GitLab Pages and create a -repository named `username.example.io`, where `username` is your username on -GitLab. If the first part of the project name doesn't match exactly your -username, it won’t work, so make sure to get it right. - -To create a group page, the steps are the same like when creating a website for -users. Just make sure that you are creating the project within the group's -namespace. - -![Create a user-based pages project](img/pages_create_user_page.png) - ---- - -After you push some static content to your repository and GitLab Runner uploads -the artifacts to GitLab CI, you will be able to access your website under -`http(s)://username.example.io`. Keep reading to find out how. - ->**Note:** -If your username/groupname contains a dot, for example `foo.bar`, you will not -be able to use the wildcard domain HTTPS, read more at [limitations](#limitations). - -### Project Pages - -GitLab Pages for projects can be created by both user and group accounts. -The steps to create a project page for a user or a group are identical: - -1. Create a new project -1. Push a [`.gitlab-ci.yml` file][yaml] in the root directory - of your repository with a specific job named [`pages`][pages]. -1. Set up a GitLab Runner to build your website - -A user's project will be served under `http(s)://username.example.io/projectname` -whereas a group's project under `http(s)://groupname.example.io/projectname`. - -## Quick Start - -Read through [GitLab Pages Quick Start Guide][pages-quick] or watch the video tutorial on -[how to publish a website with GitLab Pages on GitLab.com from a forked project][video-pages-fork]. - -See also [All you Need to Know About GitLab Pages][pages-index-guide] for a list with all the resources we have for GitLab Pages. - -### Explore the contents of `.gitlab-ci.yml` - -The key thing about GitLab Pages is the `.gitlab-ci.yml` file, something that -gives you absolute control over the build process. You can actually watch your -website being built live by following the CI job traces. - -> **Note:** -> Before reading this section, make sure you familiarize yourself with GitLab CI -> and the specific syntax of[`.gitlab-ci.yml`][yaml] by -> following our [quick start guide]. - -To make use of GitLab Pages, the contents of `.gitlab-ci.yml` must follow the -rules below: - -1. A special job named [`pages`][pages] must be defined -1. Any static content which will be served by GitLab Pages must be placed under - a `public/` directory -1. `artifacts` with a path to the `public/` directory must be defined - -In its simplest form, `.gitlab-ci.yml` looks like: - -```yaml -pages: - script: - - my_commands - artifacts: - paths: - - public -``` - -When the Runner reaches to build the `pages` job, it executes whatever is -defined in the `script` parameter and if the job completes with a non-zero -exit status, it then uploads the `public/` directory to GitLab Pages. - -The `public/` directory should contain all the static content of your website. -Depending on how you plan to publish your website, the steps defined in the -[`script` parameter](../../../ci/yaml/README.md#script) may differ. - -Be aware that Pages are by default branch/tag agnostic and their deployment -relies solely on what you specify in `.gitlab-ci.yml`. If you don't limit the -`pages` job with the [`only` parameter](../../../ci/yaml/README.md#only-and-except), -whenever a new commit is pushed to whatever branch or tag, the Pages will be -overwritten. In the example below, we limit the Pages to be deployed whenever -a commit is pushed only on the `master` branch: - -```yaml -pages: - script: - - my_commands - artifacts: - paths: - - public - only: - - master -``` - -We then tell the Runner to treat the `public/` directory as `artifacts` and -upload it to GitLab. And since all these parameters were all under a `pages` -job, the contents of the `public` directory will be served by GitLab Pages. - -#### How `.gitlab-ci.yml` looks like when the static content is in your repository - -Supposedly your repository contained the following files: - -``` -├── index.html -├── css -│ └── main.css -└── js - └── main.js -``` - -Then the `.gitlab-ci.yml` example below simply moves all files from the root -directory of the project to the `public/` directory. The `.public` workaround -is so `cp` doesn't also copy `public/` to itself in an infinite loop: - -```yaml -pages: - script: - - mkdir .public - - cp -r * .public - - mv .public public - artifacts: - paths: - - public - only: - - master -``` - -#### How `.gitlab-ci.yml` looks like when using a static generator - -In general, GitLab Pages support any kind of [static site generator][staticgen], -since `.gitlab-ci.yml` can be configured to run any possible command. - -In the root directory of your Git repository, place the source files of your -favorite static generator. Then provide a `.gitlab-ci.yml` file which is -specific to your static generator. - -The example below, uses [Jekyll] to build the static site: - -```yaml -image: ruby:2.1 # the script will run in Ruby 2.1 using the Docker image ruby:2.1 - -pages: # the build job must be named pages - script: - - gem install jekyll # we install jekyll - - jekyll build -d public/ # we tell jekyll to build the site for us - artifacts: - paths: - - public # this is where the site will live and the Runner uploads it in GitLab - only: - - master # this script is only affecting the master branch -``` - -Here, we used the Docker executor and in the first line we specified the base -image against which our jobs will run. - -You have to make sure that the generated static files are ultimately placed -under the `public` directory, that's why in the `script` section we run the -`jekyll` command that jobs the website and puts all content in the `public/` -directory. Depending on the static generator of your choice, this command will -differ. Search in the documentation of the static generator you will use if -there is an option to explicitly set the output directory. If there is not -such an option, you can always add one more line under `script` to rename the -resulting directory in `public/`. - -We then tell the Runner to treat the `public/` directory as `artifacts` and -upload it to GitLab. - ---- - -See the [jekyll example project][pages-jekyll] to better understand how this -works. - -For a list of Pages projects, see the [example projects](#example-projects) to -get you started. - -#### How to set up GitLab Pages in a repository where there's also actual code - -Remember that GitLab Pages are by default branch/tag agnostic and their -deployment relies solely on what you specify in `.gitlab-ci.yml`. You can limit -the `pages` job with the [`only` parameter](../../../ci/yaml/README.md#only-and-except), -whenever a new commit is pushed to a branch that will be used specifically for -your pages. - -That way, you can have your project's code in the `master` branch and use an -orphan branch (let's name it `pages`) that will host your static generator site. - -You can create a new empty branch like this: - -```bash -git checkout --orphan pages -``` - -The first commit made on this new branch will have no parents and it will be -the root of a new history totally disconnected from all the other branches and -commits. Push the source files of your static generator in the `pages` branch. - -Below is a copy of `.gitlab-ci.yml` where the most significant line is the last -one, specifying to execute everything in the `pages` branch: - -``` -image: ruby:2.1 - -pages: - script: - - gem install jekyll - - jekyll build -d public/ - artifacts: - paths: - - public - only: - - pages -``` - -See an example that has different files in the [`master` branch][jekyll-master] -and the source files for Jekyll are in a [`pages` branch][jekyll-pages] which -also includes `.gitlab-ci.yml`. - -[jekyll-master]: https://gitlab.com/pages/jekyll-branched/tree/master -[jekyll-pages]: https://gitlab.com/pages/jekyll-branched/tree/pages - -## Next steps - -So you have successfully deployed your website, congratulations! Let's check -what more you can do with GitLab Pages. - -### Example projects - -Below is a list of example projects for GitLab Pages with a plain HTML website -or various static site generators. Contributions are very welcome. - -- [Plain HTML](https://gitlab.com/pages/plain-html) -- [Jekyll](https://gitlab.com/pages/jekyll) -- [Hugo](https://gitlab.com/pages/hugo) -- [Middleman](https://gitlab.com/pages/middleman) -- [Hexo](https://gitlab.com/pages/hexo) -- [Brunch](https://gitlab.com/pages/brunch) -- [Metalsmith](https://gitlab.com/pages/metalsmith) -- [Harp](https://gitlab.com/pages/harp) - -Visit the GitLab Pages group for a full list of example projects: -<https://gitlab.com/groups/pages>. - -### Add a custom domain to your Pages website - -If this setting is enabled by your GitLab administrator, you should be able to -see the **New Domain** button when visiting your project's settings through the -gear icon in the top right and then navigating to **Pages**. - -![New domain button](img/pages_new_domain_button.png) - ---- - -You can add multiple domains pointing to your website hosted under GitLab. -Once the domain is added, you can see it listed under the **Domains** section. - -![Pages multiple domains](img/pages_multiple_domains.png) - ---- - -As a last step, you need to configure your DNS and add a CNAME pointing to your -user/group page. Click on the **Details** button of a domain for further -instructions. - -![Pages DNS details](img/pages_dns_details.png) - ---- - ->**Note:** -Currently there is support only for custom domains on per-project basis. That -means that if you add a custom domain (`example.com`) for your user website -(`username.example.io`), a project that is served under `username.example.io/foo`, -will not be accessible under `example.com/foo`. - -### Secure your custom domain website with TLS - -When you add a new custom domain, you also have the chance to add a TLS -certificate. If this setting is enabled by your GitLab administrator, you -should be able to see the option to upload the public certificate and the -private key when adding a new domain. - -![Pages upload cert](img/pages_upload_cert.png) - -### Custom error codes pages - -You can provide your own 403 and 404 error pages by creating the `403.html` and -`404.html` files respectively in the root directory of the `public/` directory -that will be included in the artifacts. Usually this is the root directory of -your project, but that may differ depending on your static generator -configuration. - -If the case of `404.html`, there are different scenarios. For example: - -- If you use project Pages (served under `/projectname/`) and try to access - `/projectname/non/exsiting_file`, GitLab Pages will try to serve first - `/projectname/404.html`, and then `/404.html`. -- If you use user/group Pages (served under `/`) and try to access - `/non/existing_file` GitLab Pages will try to serve `/404.html`. -- If you use a custom domain and try to access `/non/existing_file`, GitLab - Pages will try to serve only `/404.html`. - -### Remove the contents of your pages - -If you ever feel the need to purge your Pages content, you can do so by going -to your project's settings through the gear icon in the top right, and then -navigating to **Pages**. Hit the **Remove pages** button and your Pages website -will be deleted. Simple as that. - -![Remove pages](img/pages_remove.png) - -## GitLab Pages on GitLab.com - -If you are using GitLab.com to host your website, then: - -- The general domain name for GitLab Pages on GitLab.com is `gitlab.io`. -- Custom domains and TLS support are enabled. -- Shared runners are enabled by default, provided for free and can be used to - build your website. If you want you can still bring your own Runner. - -The rest of the guide still applies. - -## Limitations - -When using Pages under the general domain of a GitLab instance (`*.example.io`), -you _cannot_ use HTTPS with sub-subdomains. That means that if your -username/groupname contains a dot, for example `foo.bar`, the domain -`https://foo.bar.example.io` will _not_ work. This is a limitation of the -[HTTP Over TLS protocol][rfc]. HTTP pages will continue to work provided you -don't redirect HTTP to HTTPS. - -[rfc]: https://tools.ietf.org/html/rfc2818#section-3.1 "HTTP Over TLS RFC" - -## Redirects in GitLab Pages - -Since you cannot use any custom server configuration files, like `.htaccess` or -any `.conf` file for that matter, if you want to redirect a web page to another -location, you can use the [HTTP meta refresh tag][metarefresh]. - -Some static site generators provide plugins for that functionality so that you -don't have to create and edit HTML files manually. For example, Jekyll has the -[redirect-from plugin](https://github.com/jekyll/jekyll-redirect-from). - -## Frequently Asked Questions - -### Can I download my generated pages? - -Sure. All you need to do is download the artifacts archive from the job page. - -### Can I use GitLab Pages if my project is private? - -Yes. GitLab Pages don't care whether you set your project's visibility level -to private, internal or public. - -### Do I need to create a user/group website before creating a project website? - -No, you don't. You can create your project first and it will be accessed under -`http(s)://namespace.example.io/projectname`. - -## Known issues - -For a list of known issues, visit GitLab's [public issue tracker]. - ---- - -[jekyll]: http://jekyllrb.com/ -[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 -[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173 -[pages-daemon]: https://gitlab.com/gitlab-org/gitlab-pages -[gitlab ci]: https://about.gitlab.com/gitlab-ci -[gitlab runner]: https://docs.gitlab.com/runner -[pages]: ../../../ci/yaml/README.md#pages -[yaml]: ../../../ci/yaml/README.md -[staticgen]: https://www.staticgen.com/ -[pages-jekyll]: https://gitlab.com/pages/jekyll -[metarefresh]: https://en.wikipedia.org/wiki/Meta_refresh -[public issue tracker]: https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name=Pages -[ce-14605]: https://gitlab.com/gitlab-org/gitlab-ce/issues/14605 -[quick start guide]: ../../../ci/quick_start/README.md -[pages-index-guide]: ../../../pages/index.md -[pages-quick]: ../../../pages/getting_started_part_one.md -[video-pages-fork]: https://youtu.be/TWqh9MtT4Bg +# GitLab Pages documentation + +With GitLab Pages you can create static websites for your GitLab projects, +groups, or user accounts. You can use any static website generator: Jekyll, +Middleman, Hexo, Hugo, Pelican, you name it! Connect as many customs domains +as you like and bring your own TLS certificate to secure them. + +Here's some info we've gathered to get you started. + +## General info + +- [Product webpage](https://pages.gitlab.io) +- ["We're bringing GitLab Pages to CE" blog post](https://about.gitlab.com/2016/12/24/were-bringing-gitlab-pages-to-community-edition/) +- [Pages group - templates](https://gitlab.com/pages) +- [General user documentation](introduction.md) +- [Admin documentation - Set GitLab Pages on your own GitLab instance](../../../administration/pages/index.md) + +## Getting started + +- **GitLab Pages from A to Z** + - [Part 1: Static sites and GitLab Pages domains](getting_started_part_one.md) + - [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md) + - [Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](getting_started_part_three.md) + - [Part 4: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_four.md) +- **Static Site Generators - Blog posts series** + - [SSGs part 1: Static vs dynamic websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/) + - [SSGs part 2: Modern static site generators](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/) + - [SSGs part 3: Build any SSG site with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) +- **Secure GitLab Pages custom domain with SSL/TLS certificates** + - [Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/) + - [CloudFlare](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) + - [StartSSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/) +- **General** + - [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) a comprehensive step-by-step guide + - [Posting to your GitLab Pages blog from iOS](https://about.gitlab.com/2016/08/19/posting-to-your-gitlab-pages-blog-from-ios/) + +## Video tutorials + +- [How to publish a website with GitLab Pages on GitLab.com: from a forked project](https://youtu.be/TWqh9MtT4Bg) +- [How to Enable GitLab Pages for GitLab CE and EE (for Admins only)](https://youtu.be/dD8c7WNcc6s) + +## Advanced use + +- **Blog Posts** + - [GitLab CI: Run jobs sequentially, in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/) + - [GitLab CI: Deployment & environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/) + - [Building a new GitLab docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/) + - [Publish code coverage reports with GitLab Pages](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/) diff --git a/doc/user/project/pages/introduction.md b/doc/user/project/pages/introduction.md new file mode 100644 index 00000000000..deaceabb7c5 --- /dev/null +++ b/doc/user/project/pages/introduction.md @@ -0,0 +1,447 @@ +# GitLab Pages + +> **Notes:** +> - This feature was [introduced][ee-80] in GitLab EE 8.3. +> - Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5. +> - GitLab Pages [were ported][ce-14605] to Community Edition in GitLab 8.17. +> - This document is about the user guide. To learn how to enable GitLab Pages +> across your GitLab instance, visit the [administrator documentation](../../../administration/pages/index.md). + +With GitLab Pages you can host for free your static websites on GitLab. +Combined with the power of [GitLab CI] and the help of [GitLab Runner] you can +deploy static pages for your individual projects, your user or your group. + +Read [GitLab Pages on GitLab.com](#gitlab-pages-on-gitlab-com) for specific +information, if you are using GitLab.com to host your website. + +Read through [All you Need to Know About GitLab Pages][pages-index-guide] for a list of all learning materials we have prepared for GitLab Pages (webpages, articles, guides, blog posts, video tutorials). + +## Getting started with GitLab Pages + +> **Note:** +> In the rest of this document we will assume that the general domain name that +> is used for GitLab Pages is `example.io`. + +In general there are two types of pages one might create: + +- Pages per user (`username.example.io`) or per group (`groupname.example.io`) +- Pages per project (`username.example.io/projectname` or `groupname.example.io/projectname`) + +In GitLab, usernames and groupnames are unique and we often refer to them +as namespaces. There can be only one namespace in a GitLab instance. Below you +can see the connection between the type of GitLab Pages, what the project name +that is created on GitLab looks like and the website URL it will be ultimately +be served on. + +| Type of GitLab Pages | The name of the project created in GitLab | Website URL | +| -------------------- | ------------ | ----------- | +| User pages | `username.example.io` | `http(s)://username.example.io` | +| Group pages | `groupname.example.io` | `http(s)://groupname.example.io` | +| Project pages owned by a user | `projectname` | `http(s)://username.example.io/projectname` | +| Project pages owned by a group | `projectname` | `http(s)://groupname.example.io/projectname`| + +> **Warning:** +> There are some known [limitations](#limitations) regarding namespaces served +> under the general domain name and HTTPS. Make sure to read that section. + +### GitLab Pages requirements + +In brief, this is what you need to upload your website in GitLab Pages: + +1. Find out the general domain name that is used for GitLab Pages + (ask your administrator). This is very important, so you should first make + sure you get that right. +1. Create a project +1. Push a [`.gitlab-ci.yml` file][yaml] in the root directory + of your repository with a specific job named [`pages`][pages] +1. Set up a GitLab Runner to build your website + +> **Note:** +If [shared runners](../../../ci/runners/README.md) are enabled by your GitLab +administrator, you should be able to use them instead of bringing your own. + +### User or group Pages + +For user and group pages, the name of the project should be specific to the +username or groupname and the general domain name that is used for GitLab Pages. +Head over your GitLab instance that supports GitLab Pages and create a +repository named `username.example.io`, where `username` is your username on +GitLab. If the first part of the project name doesn't match exactly your +username, it won’t work, so make sure to get it right. + +To create a group page, the steps are the same like when creating a website for +users. Just make sure that you are creating the project within the group's +namespace. + +![Create a user-based pages project](img/pages_create_user_page.png) + +--- + +After you push some static content to your repository and GitLab Runner uploads +the artifacts to GitLab CI, you will be able to access your website under +`http(s)://username.example.io`. Keep reading to find out how. + +>**Note:** +If your username/groupname contains a dot, for example `foo.bar`, you will not +be able to use the wildcard domain HTTPS, read more at [limitations](#limitations). + +### Project Pages + +GitLab Pages for projects can be created by both user and group accounts. +The steps to create a project page for a user or a group are identical: + +1. Create a new project +1. Push a [`.gitlab-ci.yml` file][yaml] in the root directory + of your repository with a specific job named [`pages`][pages]. +1. Set up a GitLab Runner to build your website + +A user's project will be served under `http(s)://username.example.io/projectname` +whereas a group's project under `http(s)://groupname.example.io/projectname`. + +## Quick Start + +Read through [GitLab Pages Quick Start Guide][pages-quick] or watch the video tutorial on +[how to publish a website with GitLab Pages on GitLab.com from a forked project][video-pages-fork]. + +See also [All you Need to Know About GitLab Pages][pages-index-guide] for a list with all the resources we have for GitLab Pages. + +### Explore the contents of `.gitlab-ci.yml` + +The key thing about GitLab Pages is the `.gitlab-ci.yml` file, something that +gives you absolute control over the build process. You can actually watch your +website being built live by following the CI job traces. + +> **Note:** +> Before reading this section, make sure you familiarize yourself with GitLab CI +> and the specific syntax of[`.gitlab-ci.yml`][yaml] by +> following our [quick start guide]. + +To make use of GitLab Pages, the contents of `.gitlab-ci.yml` must follow the +rules below: + +1. A special job named [`pages`][pages] must be defined +1. Any static content which will be served by GitLab Pages must be placed under + a `public/` directory +1. `artifacts` with a path to the `public/` directory must be defined + +In its simplest form, `.gitlab-ci.yml` looks like: + +```yaml +pages: + script: + - my_commands + artifacts: + paths: + - public +``` + +When the Runner reaches to build the `pages` job, it executes whatever is +defined in the `script` parameter and if the job completes with a non-zero +exit status, it then uploads the `public/` directory to GitLab Pages. + +The `public/` directory should contain all the static content of your website. +Depending on how you plan to publish your website, the steps defined in the +[`script` parameter](../../../ci/yaml/README.md#script) may differ. + +Be aware that Pages are by default branch/tag agnostic and their deployment +relies solely on what you specify in `.gitlab-ci.yml`. If you don't limit the +`pages` job with the [`only` parameter](../../../ci/yaml/README.md#only-and-except), +whenever a new commit is pushed to whatever branch or tag, the Pages will be +overwritten. In the example below, we limit the Pages to be deployed whenever +a commit is pushed only on the `master` branch: + +```yaml +pages: + script: + - my_commands + artifacts: + paths: + - public + only: + - master +``` + +We then tell the Runner to treat the `public/` directory as `artifacts` and +upload it to GitLab. And since all these parameters were all under a `pages` +job, the contents of the `public` directory will be served by GitLab Pages. + +#### How `.gitlab-ci.yml` looks like when the static content is in your repository + +Supposedly your repository contained the following files: + +``` +├── index.html +├── css +│ └── main.css +└── js + └── main.js +``` + +Then the `.gitlab-ci.yml` example below simply moves all files from the root +directory of the project to the `public/` directory. The `.public` workaround +is so `cp` doesn't also copy `public/` to itself in an infinite loop: + +```yaml +pages: + script: + - mkdir .public + - cp -r * .public + - mv .public public + artifacts: + paths: + - public + only: + - master +``` + +#### How `.gitlab-ci.yml` looks like when using a static generator + +In general, GitLab Pages support any kind of [static site generator][staticgen], +since `.gitlab-ci.yml` can be configured to run any possible command. + +In the root directory of your Git repository, place the source files of your +favorite static generator. Then provide a `.gitlab-ci.yml` file which is +specific to your static generator. + +The example below, uses [Jekyll] to build the static site: + +```yaml +image: ruby:2.1 # the script will run in Ruby 2.1 using the Docker image ruby:2.1 + +pages: # the build job must be named pages + script: + - gem install jekyll # we install jekyll + - jekyll build -d public/ # we tell jekyll to build the site for us + artifacts: + paths: + - public # this is where the site will live and the Runner uploads it in GitLab + only: + - master # this script is only affecting the master branch +``` + +Here, we used the Docker executor and in the first line we specified the base +image against which our jobs will run. + +You have to make sure that the generated static files are ultimately placed +under the `public` directory, that's why in the `script` section we run the +`jekyll` command that jobs the website and puts all content in the `public/` +directory. Depending on the static generator of your choice, this command will +differ. Search in the documentation of the static generator you will use if +there is an option to explicitly set the output directory. If there is not +such an option, you can always add one more line under `script` to rename the +resulting directory in `public/`. + +We then tell the Runner to treat the `public/` directory as `artifacts` and +upload it to GitLab. + +--- + +See the [jekyll example project][pages-jekyll] to better understand how this +works. + +For a list of Pages projects, see the [example projects](#example-projects) to +get you started. + +#### How to set up GitLab Pages in a repository where there's also actual code + +Remember that GitLab Pages are by default branch/tag agnostic and their +deployment relies solely on what you specify in `.gitlab-ci.yml`. You can limit +the `pages` job with the [`only` parameter](../../../ci/yaml/README.md#only-and-except), +whenever a new commit is pushed to a branch that will be used specifically for +your pages. + +That way, you can have your project's code in the `master` branch and use an +orphan branch (let's name it `pages`) that will host your static generator site. + +You can create a new empty branch like this: + +```bash +git checkout --orphan pages +``` + +The first commit made on this new branch will have no parents and it will be +the root of a new history totally disconnected from all the other branches and +commits. Push the source files of your static generator in the `pages` branch. + +Below is a copy of `.gitlab-ci.yml` where the most significant line is the last +one, specifying to execute everything in the `pages` branch: + +``` +image: ruby:2.1 + +pages: + script: + - gem install jekyll + - jekyll build -d public/ + artifacts: + paths: + - public + only: + - pages +``` + +See an example that has different files in the [`master` branch][jekyll-master] +and the source files for Jekyll are in a [`pages` branch][jekyll-pages] which +also includes `.gitlab-ci.yml`. + +[jekyll-master]: https://gitlab.com/pages/jekyll-branched/tree/master +[jekyll-pages]: https://gitlab.com/pages/jekyll-branched/tree/pages + +## Next steps + +So you have successfully deployed your website, congratulations! Let's check +what more you can do with GitLab Pages. + +### Example projects + +Below is a list of example projects for GitLab Pages with a plain HTML website +or various static site generators. Contributions are very welcome. + +- [Plain HTML](https://gitlab.com/pages/plain-html) +- [Jekyll](https://gitlab.com/pages/jekyll) +- [Hugo](https://gitlab.com/pages/hugo) +- [Middleman](https://gitlab.com/pages/middleman) +- [Hexo](https://gitlab.com/pages/hexo) +- [Brunch](https://gitlab.com/pages/brunch) +- [Metalsmith](https://gitlab.com/pages/metalsmith) +- [Harp](https://gitlab.com/pages/harp) + +Visit the GitLab Pages group for a full list of example projects: +<https://gitlab.com/groups/pages>. + +### Add a custom domain to your Pages website + +If this setting is enabled by your GitLab administrator, you should be able to +see the **New Domain** button when visiting your project's settings through the +gear icon in the top right and then navigating to **Pages**. + +![New domain button](img/pages_new_domain_button.png) + +--- + +You can add multiple domains pointing to your website hosted under GitLab. +Once the domain is added, you can see it listed under the **Domains** section. + +![Pages multiple domains](img/pages_multiple_domains.png) + +--- + +As a last step, you need to configure your DNS and add a CNAME pointing to your +user/group page. Click on the **Details** button of a domain for further +instructions. + +![Pages DNS details](img/pages_dns_details.png) + +--- + +>**Note:** +Currently there is support only for custom domains on per-project basis. That +means that if you add a custom domain (`example.com`) for your user website +(`username.example.io`), a project that is served under `username.example.io/foo`, +will not be accessible under `example.com/foo`. + +### Secure your custom domain website with TLS + +When you add a new custom domain, you also have the chance to add a TLS +certificate. If this setting is enabled by your GitLab administrator, you +should be able to see the option to upload the public certificate and the +private key when adding a new domain. + +![Pages upload cert](img/pages_upload_cert.png) + +### Custom error codes pages + +You can provide your own 403 and 404 error pages by creating the `403.html` and +`404.html` files respectively in the root directory of the `public/` directory +that will be included in the artifacts. Usually this is the root directory of +your project, but that may differ depending on your static generator +configuration. + +If the case of `404.html`, there are different scenarios. For example: + +- If you use project Pages (served under `/projectname/`) and try to access + `/projectname/non/exsiting_file`, GitLab Pages will try to serve first + `/projectname/404.html`, and then `/404.html`. +- If you use user/group Pages (served under `/`) and try to access + `/non/existing_file` GitLab Pages will try to serve `/404.html`. +- If you use a custom domain and try to access `/non/existing_file`, GitLab + Pages will try to serve only `/404.html`. + +### Remove the contents of your pages + +If you ever feel the need to purge your Pages content, you can do so by going +to your project's settings through the gear icon in the top right, and then +navigating to **Pages**. Hit the **Remove pages** button and your Pages website +will be deleted. Simple as that. + +![Remove pages](img/pages_remove.png) + +## GitLab Pages on GitLab.com + +If you are using GitLab.com to host your website, then: + +- The general domain name for GitLab Pages on GitLab.com is `gitlab.io`. +- Custom domains and TLS support are enabled. +- Shared runners are enabled by default, provided for free and can be used to + build your website. If you want you can still bring your own Runner. + +The rest of the guide still applies. + +## Limitations + +When using Pages under the general domain of a GitLab instance (`*.example.io`), +you _cannot_ use HTTPS with sub-subdomains. That means that if your +username/groupname contains a dot, for example `foo.bar`, the domain +`https://foo.bar.example.io` will _not_ work. This is a limitation of the +[HTTP Over TLS protocol][rfc]. HTTP pages will continue to work provided you +don't redirect HTTP to HTTPS. + +[rfc]: https://tools.ietf.org/html/rfc2818#section-3.1 "HTTP Over TLS RFC" + +## Redirects in GitLab Pages + +Since you cannot use any custom server configuration files, like `.htaccess` or +any `.conf` file for that matter, if you want to redirect a web page to another +location, you can use the [HTTP meta refresh tag][metarefresh]. + +Some static site generators provide plugins for that functionality so that you +don't have to create and edit HTML files manually. For example, Jekyll has the +[redirect-from plugin](https://github.com/jekyll/jekyll-redirect-from). + +## Frequently Asked Questions + +### Can I download my generated pages? + +Sure. All you need to do is download the artifacts archive from the job page. + +### Can I use GitLab Pages if my project is private? + +Yes. GitLab Pages don't care whether you set your project's visibility level +to private, internal or public. + +### Do I need to create a user/group website before creating a project website? + +No, you don't. You can create your project first and it will be accessed under +`http(s)://namespace.example.io/projectname`. + +## Known issues + +For a list of known issues, visit GitLab's [public issue tracker]. + +[jekyll]: http://jekyllrb.com/ +[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 +[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173 +[pages-daemon]: https://gitlab.com/gitlab-org/gitlab-pages +[gitlab ci]: https://about.gitlab.com/gitlab-ci +[gitlab runner]: https://docs.gitlab.com/runner/ +[pages]: ../../../ci/yaml/README.md#pages +[yaml]: ../../../ci/yaml/README.md +[staticgen]: https://www.staticgen.com/ +[pages-jekyll]: https://gitlab.com/pages/jekyll +[metarefresh]: https://en.wikipedia.org/wiki/Meta_refresh +[public issue tracker]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=pages +[ce-14605]: https://gitlab.com/gitlab-org/gitlab-ce/issues/14605 +[quick start guide]: ../../../ci/quick_start/README.md +[pages-index-guide]: index.md +[pages-quick]: getting_started_part_one.md +[video-pages-fork]: https://youtu.be/TWqh9MtT4Bg diff --git a/doc/user/project/slash_commands.md b/doc/user/project/slash_commands.md index ad5d51d34f2..45176fde9db 100644 --- a/doc/user/project/slash_commands.md +++ b/doc/user/project/slash_commands.md @@ -35,3 +35,4 @@ do. | <code>/spend <1h 30m | -1h 5m></code> | Add or subtract spent time | | `/remove_time_spent` | Remove time spent | | `/target_branch <Branch Name>` | Set target branch for current merge request | +| `/award :emoji:` | Toggle award for :emoji: | diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md index 4889e3ec50c..d12c0c6d0c4 100644 --- a/doc/workflow/gitlab_flow.md +++ b/doc/workflow/gitlab_flow.md @@ -203,7 +203,7 @@ But the advantages of having stable identifiers outweigh this drawback. And to understand a change in context one can always look at the merge commit that groups all the commits together when the code is merged into the master branch. After you merge multiple commits from a feature branch into the master branch this is harder to undo. -If you would have squashed all the commits into one you could have just reverted this commit but as we indicated you should not rebase commits after they are pushed. +If you had squashed all the commits into one you could have just reverted this commit but as we indicated you should not rebase commits after they are pushed. Fortunately [reverting a merge made some time ago](https://git-scm.com/blog/2010/03/02/undoing-merges.html) can be done with git. This however, requires having specific merge commits for the commits your want to revert. If you revert a merge and you change your mind, revert the revert instead of merging again since git will not allow you to merge the code again otherwise. diff --git a/doc/workflow/shortcuts.md b/doc/workflow/shortcuts.md index 2a5e622dc7d..65e67aa1512 100644 --- a/doc/workflow/shortcuts.md +++ b/doc/workflow/shortcuts.md @@ -42,12 +42,10 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?' | Keyboard Shortcut | Description | | ----------------- | ----------- | | <kbd>g</kbd> + <kbd>p</kbd> | Go to the project's home page | -| <kbd>g</kbd> + <kbd>e</kbd> | Go to the project's activity feed | | <kbd>g</kbd> + <kbd>f</kbd> | Go to files | | <kbd>g</kbd> + <kbd>c</kbd> | Go to commits | | <kbd>g</kbd> + <kbd>b</kbd> | Go to jobs | | <kbd>g</kbd> + <kbd>n</kbd> | Go to network graph | -| <kbd>g</kbd> + <kbd>g</kbd> | Go to graphs | | <kbd>g</kbd> + <kbd>i</kbd> | Go to issues | | <kbd>g</kbd> + <kbd>m</kbd> | Go to merge requests | | <kbd>g</kbd> + <kbd>s</kbd> | Go to snippets | diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature index 92061dac7f4..b1d5e4a7acb 100644 --- a/features/dashboard/dashboard.feature +++ b/features/dashboard/dashboard.feature @@ -11,6 +11,7 @@ Feature: Dashboard And I visit dashboard page Scenario: I should see projects list + Then I should see "New Project" link Then I should see "Shop" project link Then I should see "Shop" project CI status diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature index 5c14c5db665..1dd2bdd9b36 100644 --- a/features/project/active_tab.feature +++ b/features/project/active_tab.feature @@ -80,9 +80,9 @@ Feature: Project Active Tab And no other sub tabs should be active And the active main tab should be Repository - Scenario: On Project Repository/Network - Given I visit my project's network page - Then the active sub tab should be Network + Scenario: On Project Repository/Graph + Given I visit my project's graph page + Then the active sub tab should be Graph And no other sub tabs should be active And the active main tab should be Repository diff --git a/features/project/graph.feature b/features/project/graph.feature index 63793d6f989..b25c73ad870 100644 --- a/features/project/graph.feature +++ b/features/project/graph.feature @@ -9,9 +9,10 @@ Feature: Project Graph Then page should have graphs @javascript - Scenario: I should see project commits graphs + Scenario: I should see project languages & commits graphs on commits graph url When I visit project "Shop" commits graph page Then page should have commits graphs + Then page should have languages graphs @javascript Scenario: I should see project ci graphs @@ -20,6 +21,13 @@ Feature: Project Graph Then page should have CI graphs @javascript - Scenario: I should see project languages graphs + Scenario: I should see project languages & commits graphs on language graph url When I visit project "Shop" languages graph page Then page should have languages graphs + Then page should have commits graphs + + @javascript + Scenario: I should see project languages & commits graphs on charts url + When I visit project "Shop" chart page + Then page should have languages graphs + Then page should have commits graphs diff --git a/features/project/issues/award_emoji.feature b/features/project/issues/award_emoji.feature index f0fd414a9f9..1d7adfdd2c2 100644 --- a/features/project/issues/award_emoji.feature +++ b/features/project/issues/award_emoji.feature @@ -42,4 +42,4 @@ Feature: Award Emoji @javascript Scenario: I add award emoji using regular comment Given I leave comment with a single emoji - Then I have award added + Then I have new comment with emoji added diff --git a/features/project/shortcuts.feature b/features/project/shortcuts.feature index f71f69ef060..95de63ba21a 100644 --- a/features/project/shortcuts.feature +++ b/features/project/shortcuts.feature @@ -19,17 +19,12 @@ Feature: Project Shortcuts Then the active sub tab should be Commits @javascript - Scenario: Navigate to network tab + Scenario: Navigate to graph tab Given I press "g" and "n" - Then the active sub tab should be Network + Then the active sub tab should be Graph And the active main tab should be Repository @javascript - Scenario: Navigate to graphs tab - Given I press "g" and "g" - Then the active main tab should be Graphs - - @javascript Scenario: Navigate to issues tab Given I press "g" and "i" Then the active main tab should be Issues @@ -53,8 +48,3 @@ Feature: Project Shortcuts Scenario: Navigate to project home Given I press "g" and "p" Then the active main tab should be Home - - @javascript - Scenario: Navigate to project feed - Given I press "g" and "e" - Then the active main tab should be Activity diff --git a/features/steps/project/commits/revert.rb b/features/steps/project/commits/revert.rb index 94a5d4e2e4d..c9746407344 100644 --- a/features/steps/project/commits/revert.rb +++ b/features/steps/project/commits/revert.rb @@ -36,5 +36,6 @@ class Spinach::Features::RevertCommits < Spinach::FeatureSteps step 'I should see the new merge request notice' do page.should have_content('The commit has been successfully reverted. You can now submit a merge request to get this change into the original branch.') + page.should have_content("From revert-#{Commit.truncate_sha(sample_commit.id)} into master") end end diff --git a/features/steps/project/graph.rb b/features/steps/project/graph.rb index 48ac7a98f0d..176d04d721c 100644 --- a/features/steps/project/graph.rb +++ b/features/steps/project/graph.rb @@ -18,6 +18,10 @@ class Spinach::Features::ProjectGraph < Spinach::FeatureSteps visit languages_namespace_project_graph_path(project.namespace, project, "master") end + step 'I visit project "Shop" chart page' do + visit charts_namespace_project_graph_path(project.namespace, project, "master") + end + step 'page should have languages graphs' do expect(page).to have_content /Ruby 66.* %/ expect(page).to have_content /JavaScript 22.* %/ diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb index cbe5738e7e4..dd7a58b454a 100644 --- a/features/steps/project/issues/award_emoji.rb +++ b/features/steps/project/issues/award_emoji.rb @@ -44,6 +44,10 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps end end + step 'I have new comment with emoji added' do + expect(page).to have_selector ".emoji[title=':smile:']" + end + step 'I have award added' do page.within '.awards' do expect(page).to have_selector '.js-emoji-btn' diff --git a/features/steps/project/network_graph.rb b/features/steps/project/network_graph.rb index ff9251615c9..370e46265c7 100644 --- a/features/steps/project/network_graph.rb +++ b/features/steps/project/network_graph.rb @@ -66,7 +66,7 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps end step 'page should have "v1.0.0" in title' do - expect(page).to have_css 'title', text: 'Network · v1.0.0', visible: false + expect(page).to have_css 'title', text: 'Graph · v1.0.0', visible: false end step 'page should only have content from "v1.0.0"' do diff --git a/features/steps/project/project_shortcuts.rb b/features/steps/project/project_shortcuts.rb index 8143b01ca40..02c08b784bc 100644 --- a/features/steps/project/project_shortcuts.rb +++ b/features/steps/project/project_shortcuts.rb @@ -34,9 +34,4 @@ class Spinach::Features::ProjectShortcuts < Spinach::FeatureSteps find('body').native.send_key('g') find('body').native.send_key('w') end - - step 'I press "g" and "e"' do - find('body').native.send_key('g') - find('body').native.send_key('e') - end end diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index 718cf924729..d5b3bb34d7a 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -232,7 +232,7 @@ module SharedPaths visit stats_namespace_project_repository_path(@project.namespace, @project) end - step "I visit my project's network page" do + step "I visit my project's graph page" do # Stub Graph max_size to speed up test (10 commits vs. 650) Network::Graph.stub(max_count: 10) diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb index d6024212601..83446afe424 100644 --- a/features/steps/shared/project_tab.rb +++ b/features/steps/shared/project_tab.rb @@ -12,10 +12,6 @@ module SharedProjectTab ensure_active_main_tab('Repository') end - step 'the active main tab should be Graphs' do - ensure_active_main_tab('Graphs') - end - step 'the active main tab should be Issues' do ensure_active_main_tab('Issues') end @@ -40,12 +36,8 @@ module SharedProjectTab expect(page).to have_selector('.layout-nav .nav-links > li.active', count: 0) end - step 'the active main tab should be Activity' do - ensure_active_main_tab('Activity') - end - - step 'the active sub tab should be Network' do - ensure_active_sub_tab('Network') + step 'the active sub tab should be Graph' do + ensure_active_sub_tab('Graph') end step 'the active sub tab should be Files' do diff --git a/lib/api/api.rb b/lib/api/api.rb index 7aa95a4a3c1..3e53ab693ab 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -5,10 +5,13 @@ module API version %w(v3 v4), using: :path version 'v3', using: :path do + mount ::API::V3::AwardEmoji mount ::API::V3::Boards mount ::API::V3::Branches + mount ::API::V3::BroadcastMessages mount ::API::V3::Commits mount ::API::V3::DeployKeys + mount ::API::V3::Environments mount ::API::V3::Files mount ::API::V3::Groups mount ::API::V3::Issues @@ -18,15 +21,22 @@ module API mount ::API::V3::MergeRequests mount ::API::V3::Notes 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::Todos mount ::API::V3::Templates + mount ::API::V3::Todos + mount ::API::V3::Triggers mount ::API::V3::Users + mount ::API::V3::Variables end before { allow_access_with_scope :api } diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index c11f8529183..409cb5b924f 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -160,13 +160,10 @@ module API # Exceptions # - class MissingTokenError < StandardError; end - - class TokenNotFoundError < StandardError; end - - class ExpiredError < StandardError; end - - class RevokedError < StandardError; end + MissingTokenError = Class.new(StandardError) + TokenNotFoundError = Class.new(StandardError) + ExpiredError = Class.new(StandardError) + RevokedError = Class.new(StandardError) class InsufficientScopeError < StandardError attr_reader :scopes diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb index 301271118d4..07a1bcdbe18 100644 --- a/lib/api/award_emoji.rb +++ b/lib/api/award_emoji.rb @@ -83,7 +83,6 @@ module API unauthorized! unless award.user == current_user || current_user.admin? award.destroy - present award, with: Entities::AwardEmoji end end end diff --git a/lib/api/boards.rb b/lib/api/boards.rb index f4226e5a89d..b6843c1b6af 100644 --- a/lib/api/boards.rb +++ b/lib/api/boards.rb @@ -127,9 +127,7 @@ module API service = ::Boards::Lists::DestroyService.new(user_project, current_user) - if service.execute(list) - present list, with: Entities::List - else + unless service.execute(list) render_api_error!({ error: 'List could not be deleted!' }, 400) end end diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 34f136948c2..73a7e939627 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -124,11 +124,7 @@ module API result = DeleteBranchService.new(user_project, current_user). execute(params[:branch]) - if result[:status] == :success - { - branch: params[:branch] - } - else + if result[:status] != :success render_api_error!(result[:message], result[:return_code]) end end diff --git a/lib/api/broadcast_messages.rb b/lib/api/broadcast_messages.rb index 1217002bf8e..395c401203c 100644 --- a/lib/api/broadcast_messages.rb +++ b/lib/api/broadcast_messages.rb @@ -91,7 +91,7 @@ module API delete ':id' do message = find_message - present message.destroy, with: Entities::BroadcastMessage + message.destroy end end end diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index dba0831664c..9d9f82fdb83 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -72,14 +72,15 @@ module API status = GenericCommitStatus.running_or_pending.find_or_initialize_by( project: @project, pipeline: pipeline, - user: current_user, name: name, ref: ref, - target_url: params[:target_url], - description: params[:description], - coverage: params[:coverage] + user: current_user ) + optional_attributes = + attributes_for_keys(%w[target_url description coverage]) + + status.update(optional_attributes) if optional_attributes.any? render_validation_error!(status) if status.invalid? begin diff --git a/lib/api/commits.rb b/lib/api/commits.rb index fd03e92264d..b0aa10f8bf2 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -127,7 +127,7 @@ module API commit_params = { commit: commit, - create_merge_request: false, + start_branch: params[:branch], target_branch: params[:branch] } diff --git a/lib/api/entities.rb b/lib/api/entities.rb index a99d9cadc8a..d2d21f5d03a 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -69,9 +69,8 @@ module API 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 :visibility, :ssh_url_to_repo, :http_url_to_repo, :web_url expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group } expose :name, :name_with_namespace expose :path, :path_with_namespace @@ -98,7 +97,7 @@ module API expose :shared_with_groups do |project, options| SharedGroup.represent(project.project_group_links.all, options) end - expose :only_allow_merge_if_build_succeeds + expose :only_allow_merge_if_pipeline_succeeds expose :request_access_enabled expose :only_allow_merge_if_all_discussions_are_resolved @@ -132,7 +131,7 @@ module API end class Group < Grape::Entity - expose :id, :name, :path, :description, :visibility_level + expose :id, :name, :path, :description, :visibility expose :lfs_enabled?, as: :lfs_enabled expose :avatar_url expose :web_url @@ -288,7 +287,7 @@ module API expose :label_names, as: :labels expose :work_in_progress?, as: :work_in_progress expose :milestone, using: Entities::Milestone - expose :merge_when_build_succeeds + expose :merge_when_pipeline_succeeds expose :merge_status expose :diff_head_sha, as: :sha expose :merge_commit_sha @@ -394,7 +393,8 @@ module API expose :target_type expose :target do |todo, options| - Entities.const_get(todo.target_type).represent(todo.target, options) + target = todo.target_type == 'Commit' ? 'RepoCommit' : todo.target_type + Entities.const_get(target).represent(todo.target, options) end expose :target_url do |todo, options| @@ -551,12 +551,14 @@ module API expose :updated_at expose :home_page_url expose :default_branch_protection - expose :restricted_visibility_levels + expose(:restricted_visibility_levels) do |setting, _options| + setting.restricted_visibility_levels.map { |level| Gitlab::VisibilityLevel.string_level(level) } + end expose :max_attachment_size expose :session_expire_delay - expose :default_project_visibility - expose :default_snippet_visibility - expose :default_group_visibility + expose(:default_project_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_project_visibility) } + expose(:default_snippet_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_snippet_visibility) } + expose(:default_group_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_group_visibility) } expose :default_artifacts_expire_in expose :domain_whitelist expose :domain_blacklist_enabled diff --git a/lib/api/environments.rb b/lib/api/environments.rb index 1a7e68f0528..ebe8c3a5b2c 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -79,7 +79,24 @@ module API environment = user_project.environments.find(params[:environment_id]) - present environment.destroy, with: Entities::Environment + environment.destroy + end + + desc 'Stops an existing environment' do + success Entities::Environment + end + params do + requires :environment_id, type: Integer, desc: 'The environment ID' + end + post ':id/environments/:environment_id/stop' do + authorize! :create_deployment, user_project + + environment = user_project.environments.find(params[:environment_id]) + + environment.stop_with_action!(current_user) + + status 200 + present environment, with: Entities::Environment end end end diff --git a/lib/api/files.rb b/lib/api/files.rb index 500f9d3c787..9c4e43d77cc 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -118,10 +118,7 @@ module API file_params = declared_params(include_missing: false) result = ::Files::DestroyService.new(user_project, current_user, commit_params(file_params)).execute - if result[:status] == :success - status(200) - commit_response(file_params) - else + if result[:status] != :success render_api_error!(result[:message], 400) end end diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 9cffd6180ae..b862ff70b31 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -7,7 +7,7 @@ module API 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 :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility 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 @@ -92,7 +92,7 @@ module API 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, + at_least_one_of :name, :path, :description, :visibility, :lfs_enabled, :request_access_enabled end put ':id' do @@ -126,7 +126,7 @@ module API end params do optional :archived, type: Boolean, default: false, desc: 'Limit by archived status' - optional :visibility, type: String, values: %w[public internal private], + optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, 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], diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 72d2b320077..9c41146f1e3 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -164,6 +164,10 @@ module API items.where(iid: iid) end + def filter_by_search(items, text) + items.search(text) + end + # error helpers def forbidden!(reason = nil) @@ -248,6 +252,10 @@ module API # 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 diff --git a/lib/api/internal.rb b/lib/api/internal.rb index d235977fbd8..7eed93aba00 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -132,6 +132,18 @@ module API { success: true, recovery_codes: codes } end + + post "/notify_post_receive" do + status 200 + + return unless Gitlab::GitalyClient.enabled? + + begin + Gitlab::GitalyClient::Notifications.new.post_receive(params[:repo_path]) + rescue GRPC::Unavailable => e + render_api_error(e, 500) + end + end end end end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 6d30c5d81b1..1d6d0b05750 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -25,6 +25,7 @@ module API 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' + optional :iids, type: Array[Integer], desc: 'The IID array of issues' use :pagination end diff --git a/lib/api/labels.rb b/lib/api/labels.rb index d2955af3f95..59f0e7cb647 100644 --- a/lib/api/labels.rb +++ b/lib/api/labels.rb @@ -1,7 +1,7 @@ module API class Labels < Grape::API include PaginationParams - + before { authenticate! } params do @@ -56,7 +56,7 @@ module API label = user_project.labels.find_by(title: params[:name]) not_found!('Label') unless label - present label.destroy, with: Entities::Label, current_user: current_user, project: user_project + label.destroy end desc 'Update an existing label. At least one optional parameter is required.' do diff --git a/lib/api/members.rb b/lib/api/members.rb index 5f6913d1a27..baf85e6075a 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -93,24 +93,10 @@ module API end delete ":id/members/:user_id" do source = find_source(source_type, params[:id]) + # Ensure that memeber exists + source.members.find_by!(user_id: params[:user_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? - { message: "Access revoked", id: params[:user_id].to_i } - else - ::Members::DestroyService.new(source, current_user, declared_params).execute - - present member.user, with: Entities::Member, member: member - end + ::Members::DestroyService.new(source, current_user, declared_params).execute end end end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index bdd764abfeb..4638a66811d 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -168,8 +168,8 @@ module API 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 pipeline succeeds' + 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' end put ':id/merge_requests/:merge_request_id/merge' do @@ -192,7 +192,7 @@ module API should_remove_source_branch: params[:should_remove_source_branch] } - if params[:merge_when_build_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active? + if params[:merge_when_pipeline_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active? ::MergeRequests::MergeWhenPipelineSucceedsService .new(merge_request.target_project, current_user, merge_params) .execute(merge_request) @@ -208,10 +208,10 @@ module API desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do success Entities::MergeRequest end - post ':id/merge_requests/:merge_request_id/cancel_merge_when_build_succeeds' do + post ':id/merge_requests/:merge_request_id/cancel_merge_when_pipeline_succeeds' do merge_request = find_project_merge_request(params[:merge_request_id]) - unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user) + unauthorized! unless merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user) ::MergeRequest::MergeWhenPipelineSucceedsService .new(merge_request.target_project, current_user) diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb index 0b4ed76b35c..bd74174c655 100644 --- a/lib/api/milestones.rb +++ b/lib/api/milestones.rb @@ -30,7 +30,8 @@ module API 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' + optional :iids, type: Array[Integer], desc: 'The IIDs of the milestones' + optional :search, type: String, desc: 'The search criteria for the title or description of the milestone' use :pagination end get ":id/milestones" do @@ -38,7 +39,8 @@ module API milestones = user_project.milestones milestones = filter_milestones_state(milestones, params[:state]) - milestones = filter_by_iid(milestones, params[:iid]) if params[:iid].present? + milestones = filter_by_iid(milestones, params[:iids]) if params[:iids].present? + milestones = filter_by_search(milestones, params[:search]) if params[:search] present paginate(milestones), with: Entities::Milestone end diff --git a/lib/api/notes.rb b/lib/api/notes.rb index f559a7f74a0..3b3e45cbd06 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -132,8 +132,6 @@ module API authorize! :admin_note, note ::Notes::DestroyService.new(user_project, current_user).execute(note) - - present note, with: Entities::Note end end end diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb index f7a28d7ad10..57a5f97dc7f 100644 --- a/lib/api/project_hooks.rb +++ b/lib/api/project_hooks.rb @@ -90,12 +90,9 @@ module API 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: Entities::ProjectHook - rescue - # ProjectHook can raise Error if hook_id not found - not_found!("Error deleting hook #{params[:hook_id]}") - end + hook = user_project.hooks.find(params.delete(:hook_id)) + + hook.destroy end end end diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index 2a1cce73f3f..f57e7ea4032 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -50,11 +50,9 @@ module API 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' + requires :visibility, type: String, + values: Gitlab::VisibilityLevel.string_values, + desc: 'The visibility of the snippet' end post ":id/snippets" do authorize! :create_project_snippet, user_project @@ -80,11 +78,9 @@ module API 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' + optional :visibility, type: String, + values: Gitlab::VisibilityLevel.string_values, + desc: 'The visibility of the snippet' at_least_one_of :title, :file_name, :code, :visibility_level end put ":id/snippets/:snippet_id" do diff --git a/lib/api/projects.rb b/lib/api/projects.rb index b89bddc7e29..63a4cdd5954 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -16,14 +16,10 @@ module API optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project' 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 :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 :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the project.' 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 end @@ -48,11 +44,12 @@ module API params :filter_params do optional :archived, type: Boolean, default: false, desc: 'Limit by archived status' - optional :visibility, type: String, values: %w[public internal private], + optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'Limit by visibility' - optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria' + optional :search, type: String, desc: 'Return list of projects matching the search criteria' optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user' optional :starred, type: Boolean, default: false, desc: 'Limit by starred status' + optional :membership, type: Boolean, default: false, desc: 'Limit by projects that the current user is a member of' end params :statistics_params do @@ -94,8 +91,9 @@ module API success Entities::Project end params do - requires :name, type: String, desc: 'The name of the project' + 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 @@ -207,8 +205,8 @@ module API at_least_one_of :name, :description, :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :shared_runners_enabled, :container_registry_enabled, - :lfs_enabled, :visibility_level, :public_builds, - :request_access_enabled, :only_allow_merge_if_build_succeeds, + :lfs_enabled, :visibility, :public_builds, + :request_access_enabled, :only_allow_merge_if_pipeline_succeeds, :only_allow_merge_if_all_discussions_are_resolved, :path, :default_branch end @@ -216,7 +214,7 @@ module API authorize_admin_project attrs = 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? + authorize! :change_visibility_level, user_project if attrs[:visibility].present? result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute @@ -353,7 +351,6 @@ module API not_found!('Group Link') unless link link.destroy - no_content! end desc 'Upload a file' diff --git a/lib/api/runner.rb b/lib/api/runner.rb index 804b27d40a7..47858f1866b 100644 --- a/lib/api/runner.rb +++ b/lib/api/runner.rb @@ -38,7 +38,7 @@ module API end desc 'Deletes a registered Runner' do - http_codes [[200, 'Runner was deleted'], [403, 'Forbidden']] + http_codes [[204, 'Runner was deleted'], [403, 'Forbidden']] end params do requires :token, type: String, desc: %q(Runner's authentication token) diff --git a/lib/api/runners.rb b/lib/api/runners.rb index 252e59bfa58..2e41f16f8c6 100644 --- a/lib/api/runners.rb +++ b/lib/api/runners.rb @@ -78,9 +78,8 @@ module API delete ':id' do runner = get_runner(params[:id]) authenticate_delete_runner!(runner) - runner.destroy! - present runner, with: Entities::Runner + runner.destroy! end end @@ -136,8 +135,6 @@ module API forbidden!("Only one project associated with the runner. Please remove the runner instead") if runner.projects.count == 1 runner_project.destroy - - present runner, with: Entities::Runner end end diff --git a/lib/api/services.rb b/lib/api/services.rb index ad856115485..79a5f27dc4d 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -654,9 +654,7 @@ module API hash.merge!(key => nil) end - if service.update_attributes(attrs.merge(active: false)) - true - else + unless service.update_attributes(attrs.merge(active: false)) render_api_error!('400 Bad Request', 400) end end diff --git a/lib/api/settings.rb b/lib/api/settings.rb index 936c7e0930b..d4d3229f0d1 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -21,9 +21,9 @@ module API 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 :default_project_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default project visibility' + optional :default_snippet_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default snippet visibility' + optional :default_group_visibility, type: String, values: Gitlab::VisibilityLevel.string_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' @@ -128,7 +128,9 @@ module API :housekeeping_enabled, :terminal_max_session_time end put "application/settings" do - if current_settings.update_attributes(declared_params(include_missing: false)) + attrs = declared_params(include_missing: false) + + if current_settings.update_attributes(attrs) present current_settings, with: Entities::ApplicationSetting else render_validation_error!(current_settings) diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb index ac03fbd2a3d..b93fdc62808 100644 --- a/lib/api/snippets.rb +++ b/lib/api/snippets.rb @@ -58,10 +58,10 @@ module API 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' + optional :visibility, type: String, + values: Gitlab::VisibilityLevel.string_values, + default: 'internal', + desc: 'The visibility of the snippet' end post do attrs = declared_params(include_missing: false).merge(request: request, api: true) @@ -85,10 +85,10 @@ module API 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 + optional :visibility, type: String, + values: Gitlab::VisibilityLevel.string_values, + desc: 'The visibility of the snippet' + at_least_one_of :title, :file_name, :content, :visibility end put ':id' do snippet = snippets_for_current_user.find_by(id: params.delete(:id)) @@ -118,9 +118,10 @@ module API delete ':id' do snippet = snippets_for_current_user.find_by(id: params.delete(:id)) return not_found!('Snippet') unless snippet + authorize! :destroy_personal_snippet, snippet + snippet.destroy - no_content! end desc 'Get a raw snippet' do diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb index d038a3fa828..ed7b23b474a 100644 --- a/lib/api/system_hooks.rb +++ b/lib/api/system_hooks.rb @@ -66,7 +66,7 @@ module API hook = SystemHook.find_by(id: params[:id]) not_found!('System hook') unless hook - present hook.destroy, with: Entities::Hook + hook.destroy end end end diff --git a/lib/api/tags.rb b/lib/api/tags.rb index 86759ab882f..d31ef9de26b 100644 --- a/lib/api/tags.rb +++ b/lib/api/tags.rb @@ -66,11 +66,7 @@ module API result = ::Tags::DestroyService.new(user_project, current_user). execute(params[:tag_name]) - if result[:status] == :success - { - tag_name: params[:tag_name] - } - else + if result[:status] != :success render_api_error!(result[:message], result[:return_code]) end end diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index ea0ad852633..b7c9c5f2b7f 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -93,8 +93,6 @@ module API return not_found!('Trigger') unless trigger trigger.destroy - - present trigger, with: Entities::Trigger end end end diff --git a/lib/api/users.rb b/lib/api/users.rb index 94b2b6653d2..7bb4b76f830 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -236,7 +236,7 @@ module API key = user.keys.find_by(id: params[:key_id]) not_found!('Key') unless key - present key.destroy, with: Entities::SSHKey + key.destroy end desc 'Add an email address to a specified user. Available only for admins.' do @@ -422,7 +422,7 @@ module API key = current_user.keys.find_by(id: params[:key_id]) not_found!('Key') unless key - present key.destroy, with: Entities::SSHKey + key.destroy end desc "Get the currently authenticated user's email addresses" do diff --git a/lib/api/v3/award_emoji.rb b/lib/api/v3/award_emoji.rb new file mode 100644 index 00000000000..1e35283631f --- /dev/null +++ b/lib/api/v3/award_emoji.rb @@ -0,0 +1,59 @@ +module API + module V3 + class AwardEmoji < Grape::API + include PaginationParams + + before { authenticate! } + AWARDABLES = %w[issue merge_request snippet].freeze + + resource :projects 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 'Delete a +awardables+ award emoji' do + detail 'This feature was introduced in 8.9' + success ::API::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? + + present award.destroy, with: ::API::Entities::AwardEmoji + end + end + end + end + + helpers do + 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 + end + end + end +end diff --git a/lib/api/v3/boards.rb b/lib/api/v3/boards.rb index 31d708bc2c8..b1c2a3c59f2 100644 --- a/lib/api/v3/boards.rb +++ b/lib/api/v3/boards.rb @@ -44,6 +44,27 @@ module API 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 diff --git a/lib/api/v3/branches.rb b/lib/api/v3/branches.rb index 51eb566cf7d..699e41b5537 100644 --- a/lib/api/v3/branches.rb +++ b/lib/api/v3/branches.rb @@ -19,6 +19,26 @@ module API present branches, with: ::API::Entities::RepoBranch, project: user_project 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 diff --git a/lib/api/v3/broadcast_messages.rb b/lib/api/v3/broadcast_messages.rb new file mode 100644 index 00000000000..417e4ad0b26 --- /dev/null +++ b/lib/api/v3/broadcast_messages.rb @@ -0,0 +1,31 @@ +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/commits.rb b/lib/api/v3/commits.rb index 506204b3517..d254d247042 100644 --- a/lib/api/v3/commits.rb +++ b/lib/api/v3/commits.rb @@ -130,9 +130,7 @@ module API commit_params = { commit: commit, - create_merge_request: false, - source_project: user_project, - source_branch: commit.cherry_pick_branch_name, + start_branch: params[:branch], target_branch: params[:branch] } diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb index 11d0e6dbf71..270d99a2348 100644 --- a/lib/api/v3/entities.rb +++ b/lib/api/v3/entities.rb @@ -45,6 +45,147 @@ module API 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 :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 + 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::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 + expose :web_url + expose :request_access_enabled + expose :full_name, :full_path + expose :parent_id + + 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 :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 end end end diff --git a/lib/api/v3/environments.rb b/lib/api/v3/environments.rb new file mode 100644 index 00000000000..3056b70e6ef --- /dev/null +++ b/lib/api/v3/environments.rb @@ -0,0 +1,87 @@ +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 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/groups.rb b/lib/api/v3/groups.rb index c826bc4fe0b..0aad87a3f58 100644 --- a/lib/api/v3/groups.rb +++ b/lib/api/v3/groups.rb @@ -6,13 +6,20 @@ module API 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: ::API::Entities::Group, + with: Entities::Group, current_user: current_user, ) @@ -22,8 +29,36 @@ module API 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.execute(current_user) + 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.is_admin? + end + desc 'Get list of owned groups for authenticated user' do - success ::API::Entities::Group + success Entities::Group end params do use :pagination @@ -32,6 +67,114 @@ module API 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' + optional :parent_id, type: Integer, desc: 'The parent group id for creating nested group' + 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 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 + present ::Groups::DestroyService.new(group, current_user).execute, with: Entities::GroupDetail, current_user: current_user + 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).execute(current_user) + 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" 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 diff --git a/lib/api/v3/issues.rb b/lib/api/v3/issues.rb index d0af09f0e1e..5d7dfabfcd6 100644 --- a/lib/api/v3/issues.rb +++ b/lib/api/v3/issues.rb @@ -226,6 +226,8 @@ module API not_found!('Issue') unless issue authorize!(:destroy_issue, issue) + + status(200) issue.destroy end end diff --git a/lib/api/v3/labels.rb b/lib/api/v3/labels.rb index 5c3261311bf..41f45d244e3 100644 --- a/lib/api/v3/labels.rb +++ b/lib/api/v3/labels.rb @@ -13,6 +13,21 @@ module API get ':id/labels' do present available_labels, 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 diff --git a/lib/api/v3/members.rb b/lib/api/v3/members.rb index 19f276d5484..3d4972afd9d 100644 --- a/lib/api/v3/members.rb +++ b/lib/api/v3/members.rb @@ -119,6 +119,7 @@ module API # 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(source, current_user, declared_params).execute diff --git a/lib/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb index 129f9d850e9..654e818e1b5 100644 --- a/lib/api/v3/merge_requests.rb +++ b/lib/api/v3/merge_requests.rb @@ -39,7 +39,7 @@ module API desc 'List merge requests' do detail 'iid filter is deprecated have been removed on V4' - success ::API::Entities::MergeRequest + success ::API::V3::Entities::MergeRequest end params do optional :state, type: String, values: %w[opened closed merged all], default: 'all', @@ -66,11 +66,11 @@ module API end merge_requests = merge_requests.reorder(params[:order_by] => params[:sort]) - present paginate(merge_requests), with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project + 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::Entities::MergeRequest + success ::API::V3::Entities::MergeRequest end params do requires :title, type: String, desc: 'The title of the merge request' @@ -89,7 +89,7 @@ module API merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute if merge_request.valid? - present merge_request, with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project + present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project else handle_merge_request_errors! merge_request.errors end @@ -103,6 +103,8 @@ module API merge_request = find_project_merge_request(params[:merge_request_id]) authorize!(:destroy_merge_request, merge_request) + + status(200) merge_request.destroy end @@ -114,12 +116,12 @@ module API if status == :deprecated detail DEPRECATION_MESSAGE end - success ::API::Entities::MergeRequest + 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::Entities::MergeRequest, current_user: current_user, project: user_project + 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 @@ -141,7 +143,7 @@ module API end desc 'Update a merge request' do - success ::API::Entities::MergeRequest + success ::API::V3::Entities::MergeRequest end params do optional :title, type: String, allow_blank: false, desc: 'The title of the merge request' @@ -162,21 +164,21 @@ module API merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request) if merge_request.valid? - present merge_request, with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project + 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::Entities::MergeRequest + 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 pipeline succeeds' + 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 @@ -209,16 +211,16 @@ module API .execute(merge_request) end - present merge_request, with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project + present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project end - desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do - success ::API::Entities::MergeRequest + 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_build_succeeds?(current_user) + unauthorized! unless merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user) ::MergeRequest::MergeWhenPipelineSucceedsService .new(merge_request.target_project, current_user) diff --git a/lib/api/v3/milestones.rb b/lib/api/v3/milestones.rb new file mode 100644 index 00000000000..bbc29c40ee2 --- /dev/null +++ b/lib/api/v3/milestones.rb @@ -0,0 +1,43 @@ +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 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? + + present paginate(milestones), with: ::API::Entities::Milestone + end + end + end + end +end diff --git a/lib/api/v3/project_snippets.rb b/lib/api/v3/project_snippets.rb index e03e941d30b..809ca4f37ba 100644 --- a/lib/api/v3/project_snippets.rb +++ b/lib/api/v3/project_snippets.rb @@ -121,6 +121,8 @@ module API authorize! :admin_project_snippet, snippet snippet.destroy + + status(200) end desc 'Get a raw project snippet' diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb index c3821555452..47bfc12035a 100644 --- a/lib/api/v3/projects.rb +++ b/lib/api/v3/projects.rb @@ -5,6 +5,10 @@ module API 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' @@ -25,6 +29,7 @@ module API 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 @@ -37,6 +42,12 @@ module API end attrs end + + def set_only_allow_merge_if_pipeline_succeeds! + if params.has_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 @@ -75,7 +86,7 @@ module API def present_projects(projects, options = {}) options = options.reverse_merge( - with: ::API::Entities::Project, + with: ::API::V3::Entities::Project, current_user: current_user, simple: params[:simple], ) @@ -95,7 +106,7 @@ module API use :collection_params end get '/visible' do - entity = current_user ? ::API::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails + entity = current_user ? ::API::V3::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails present_projects ProjectsFinder.new.execute(current_user), with: entity end @@ -109,7 +120,7 @@ module API authenticate! present_projects current_user.authorized_projects, - with: ::API::Entities::ProjectWithAccess + with: ::API::V3::Entities::ProjectWithAccess end desc 'Get an owned projects list for authenticated user' do @@ -123,7 +134,7 @@ module API authenticate! present_projects current_user.owned_projects, - with: ::API::Entities::ProjectWithAccess, + with: ::API::V3::Entities::ProjectWithAccess, statistics: params[:statistics] end @@ -149,11 +160,11 @@ module API get '/all' do authenticated_as_admin! - present_projects Project.all, with: ::API::Entities::ProjectWithAccess, statistics: params[:statistics] + 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::Entities::Project + success ::API::V3::Entities::Project end params do requires :query, type: String, desc: 'The project name to be searched' @@ -165,15 +176,16 @@ module API projects = search_service.objects('projects', params[:page]) projects = projects.reorder(params[:order_by] => params[:sort]) - present paginate(projects), with: ::API::Entities::Project + present paginate(projects), with: ::API::V3::Entities::Project end desc 'Create new project' do - success ::API::Entities::Project + success ::API::V3::Entities::Project end params do - requires :name, type: String, desc: 'The name of the project' + 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 @@ -182,7 +194,7 @@ module API project = ::Projects::CreateService.new(current_user, attrs).execute if project.saved? - present project, with: ::API::Entities::Project, + present project, with: ::API::V3::Entities::Project, user_can_admin_project: can?(current_user, :admin_project, project) else if project.errors[:limit_reached].present? @@ -193,7 +205,7 @@ module API end desc 'Create new project for a specified user. Only available to admin users.' do - success ::API::Entities::Project + success ::API::V3::Entities::Project end params do requires :name, type: String, desc: 'The name of the project' @@ -211,7 +223,7 @@ module API project = ::Projects::CreateService.new(user, attrs).execute if project.saved? - present project, with: ::API::Entities::Project, + present project, with: ::API::V3::Entities::Project, user_can_admin_project: can?(current_user, :admin_project, project) else render_validation_error!(project) @@ -224,10 +236,10 @@ module API end resource :projects, requirements: { id: /[^\/]+/ } do desc 'Get a single project' do - success ::API::Entities::ProjectWithAccess + success ::API::V3::Entities::ProjectWithAccess end get ":id" do - entity = current_user ? ::API::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails + 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 @@ -243,7 +255,7 @@ module API end desc 'Fork new project for the current user or provided namespace.' do - success ::API::Entities::Project + 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' @@ -269,13 +281,13 @@ module API if forked_project.errors.any? conflict!(forked_project.errors.messages) else - present forked_project, with: ::API::Entities::Project, + 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::Entities::Project + success ::API::V3::Entities::Project end params do optional :name, type: String, desc: 'The name of the project' @@ -299,7 +311,7 @@ module API result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute if result[:status] == :success - present user_project, with: ::API::Entities::Project, + 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) @@ -307,29 +319,29 @@ module API end desc 'Archive a project' do - success ::API::Entities::Project + success ::API::V3::Entities::Project end post ':id/archive' do authorize!(:archive_project, user_project) user_project.archive! - present user_project, with: ::API::Entities::Project + present user_project, with: ::API::V3::Entities::Project end desc 'Unarchive a project' do - success ::API::Entities::Project + success ::API::V3::Entities::Project end post ':id/unarchive' do authorize!(:archive_project, user_project) user_project.unarchive! - present user_project, with: ::API::Entities::Project + present user_project, with: ::API::V3::Entities::Project end desc 'Star a project' do - success ::API::Entities::Project + success ::API::V3::Entities::Project end post ':id/star' do if current_user.starred?(user_project) @@ -338,19 +350,19 @@ module API current_user.toggle_star(user_project) user_project.reload - present user_project, with: ::API::Entities::Project + present user_project, with: ::API::V3::Entities::Project end end desc 'Unstar a project' do - success ::API::Entities::Project + 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::Entities::Project + present user_project, with: ::API::V3::Entities::Project else not_modified! end @@ -359,6 +371,8 @@ module API desc 'Remove a project' delete ":id" do authorize! :remove_project, user_project + + status(200) ::Projects::DestroyService.new(user_project, current_user, {}).async_execute end @@ -384,6 +398,7 @@ module API authorize! :remove_fork_project, user_project if user_project.forked? + status(200) user_project.forked_project_link.destroy else not_modified! diff --git a/lib/api/v3/runners.rb b/lib/api/v3/runners.rb new file mode 100644 index 00000000000..8967141fe3d --- /dev/null +++ b/lib/api/v3/runners.rb @@ -0,0 +1,65 @@ +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 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.is_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_authorized_runners.exists?(runner.id) + end + end + end + end +end diff --git a/lib/api/v3/services.rb b/lib/api/v3/services.rb new file mode 100644 index 00000000000..af0a058f69b --- /dev/null +++ b/lib/api/v3/services.rb @@ -0,0 +1,573 @@ +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' + } + ], + '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' + } + ], + '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' + } + ] + } + + resource :projects 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 + end + end + end +end diff --git a/lib/api/v3/settings.rb b/lib/api/v3/settings.rb new file mode 100644 index 00000000000..748d6b97d4f --- /dev/null +++ b/lib/api/v3/settings.rb @@ -0,0 +1,137 @@ +module API + module V3 + class Settings < Grape::API + before { authenticated_as_admin! } + + helpers do + def current_settings + @current_setting ||= + (ApplicationSetting.current || 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 :signin_enabled, type: Boolean, desc: 'Flag indicating if sign in is 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, :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 + if current_settings.update_attributes(declared_params(include_missing: false)) + 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 new file mode 100644 index 00000000000..07dac7e9904 --- /dev/null +++ b/lib/api/v3/snippets.rb @@ -0,0 +1,138 @@ +module API + module V3 + class Snippets < Grape::API + include PaginationParams + + before { authenticate! } + + resource :snippets do + helpers do + def snippets_for_current_user + SnippetsFinder.new.execute(current_user, filter: :by_user, user: current_user) + end + + def public_snippets + SnippetsFinder.new.execute(current_user, filter: :public) + 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)) + return 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)) + return 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)) + return 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/system_hooks.rb b/lib/api/v3/system_hooks.rb index 391510b9ee0..5787c06fc12 100644 --- a/lib/api/v3/system_hooks.rb +++ b/lib/api/v3/system_hooks.rb @@ -13,6 +13,19 @@ module API 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 diff --git a/lib/api/v3/tags.rb b/lib/api/v3/tags.rb index 016e3d86932..6913720d9c5 100644 --- a/lib/api/v3/tags.rb +++ b/lib/api/v3/tags.rb @@ -14,6 +14,26 @@ module API tags = user_project.repository.tags.sort_by(&:name).reverse present tags, with: ::API::Entities::RepoTag, 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 diff --git a/lib/api/v3/todos.rb b/lib/api/v3/todos.rb index 4f9b5fe72a6..e60cb25e57b 100644 --- a/lib/api/v3/todos.rb +++ b/lib/api/v3/todos.rb @@ -19,6 +19,8 @@ module API 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) end diff --git a/lib/api/v3/triggers.rb b/lib/api/v3/triggers.rb new file mode 100644 index 00000000000..4051d4bca8d --- /dev/null +++ b/lib/api/v3/triggers.rb @@ -0,0 +1,30 @@ +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 do + desc 'Delete a trigger' do + success ::API::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) + return not_found!('Trigger') unless trigger + + trigger.destroy + + present trigger, with: ::API::Entities::Trigger + end + end + end + end +end diff --git a/lib/api/v3/users.rb b/lib/api/v3/users.rb index 7838cdc46a7..14f54731730 100644 --- a/lib/api/v3/users.rb +++ b/lib/api/v3/users.rb @@ -92,6 +92,25 @@ module API 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 @@ -111,6 +130,19 @@ module API 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 diff --git a/lib/api/v3/variables.rb b/lib/api/v3/variables.rb new file mode 100644 index 00000000000..0f55a14fb28 --- /dev/null +++ b/lib/api/v3/variables.rb @@ -0,0 +1,29 @@ +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 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/api/variables.rb b/lib/api/variables.rb index f623b1dfe9f..77e5d54c225 100644 --- a/lib/api/variables.rb +++ b/lib/api/variables.rb @@ -1,5 +1,4 @@ module API - # Projects variables API class Variables < Grape::API include PaginationParams @@ -81,10 +80,9 @@ module API end delete ':id/variables/:key' do variable = user_project.variables.find_by(key: params[:key]) + not_found!('Variable') unless variable - return not_found!('Variable') unless variable - - present variable.destroy, with: Entities::Variable + variable.destroy end end end diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb index c973897f420..849e1142841 100644 --- a/lib/banzai/filter/user_reference_filter.rb +++ b/lib/banzai/filter/user_reference_filter.rb @@ -133,7 +133,7 @@ module Banzai data = data_attribute(group: namespace.id) content = link_content || Group.reference_prefix + group - link_tag(url, data, content, namespace.name) + link_tag(url, data, content, namespace.full_name) end def link_to_user(user, namespace, link_content: nil) diff --git a/lib/bitbucket/error/unauthorized.rb b/lib/bitbucket/error/unauthorized.rb index 5e2eb57bb0e..efe10542f19 100644 --- a/lib/bitbucket/error/unauthorized.rb +++ b/lib/bitbucket/error/unauthorized.rb @@ -1,6 +1,5 @@ module Bitbucket module Error - class Unauthorized < StandardError - end + Unauthorized = Class.new(StandardError) end end diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb index 0e17ac24d5a..b51e76d93f2 100644 --- a/lib/ci/api/builds.rb +++ b/lib/ci/api/builds.rb @@ -217,6 +217,7 @@ module Ci build = Ci::Build.find_by_id(params[:id]) authenticate_build!(build) + status(200) build.erase_artifacts! end end diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb index 2a611a67eaf..c1fd959ef14 100644 --- a/lib/ci/api/runners.rb +++ b/lib/ci/api/runners.rb @@ -8,6 +8,8 @@ module Ci end delete "delete" do authenticate_runner! + + status(200) Ci::Runner.find_by_token(params[:token]).destroy end diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 649ee4d018b..e390919ae1d 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -1,6 +1,6 @@ module Ci class GitlabCiYamlProcessor - class ValidationError < StandardError; end + ValidationError = Class.new(StandardError) include Gitlab::Ci::Config::Entry::LegacyValidationHelpers diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb index 9ece84cc469..dd864eea3fa 100644 --- a/lib/extracts_path.rb +++ b/lib/extracts_path.rb @@ -2,7 +2,7 @@ # file path string when combined in a request parameter module ExtractsPath # Raised when given an invalid file path - class InvalidPathError < StandardError; end + InvalidPathError = Class.new(StandardError) # Given a string containing both a Git tree-ish, such as a branch or tag, and # a filesystem path joined by forward slashes, attempts to separate the two. diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb index 3b210eeda9d..8c28009b9c6 100644 --- a/lib/gitlab/access.rb +++ b/lib/gitlab/access.rb @@ -5,7 +5,7 @@ # module Gitlab module Access - class AccessDeniedError < StandardError; end + AccessDeniedError = Class.new(StandardError) NO_ACCESS = 0 GUEST = 10 diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 89db6c3da46..0a5abc92190 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -1,6 +1,6 @@ module Gitlab module Auth - class MissingPersonalTokenError < StandardError; end + MissingPersonalTokenError = Class.new(StandardError) SCOPES = [:api, :read_user].freeze DEFAULT_SCOPES = [:api].freeze diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb index cd2e83b4c27..a375ccbece0 100644 --- a/lib/gitlab/ci/build/artifacts/metadata.rb +++ b/lib/gitlab/ci/build/artifacts/metadata.rb @@ -6,7 +6,7 @@ module Gitlab module Build module Artifacts class Metadata - class ParserError < StandardError; end + ParserError = Class.new(StandardError) VERSION_PATTERN = /^[\w\s]+(\d+\.\d+\.\d+)/ INVALID_PATH_PATTERN = %r{(^\.?\.?/)|(/\.?\.?/)} diff --git a/lib/gitlab/ci/config/entry/factory.rb b/lib/gitlab/ci/config/entry/factory.rb index 9f5e393d191..6be8288748f 100644 --- a/lib/gitlab/ci/config/entry/factory.rb +++ b/lib/gitlab/ci/config/entry/factory.rb @@ -6,7 +6,7 @@ module Gitlab # Factory class responsible for fabricating entry objects. # class Factory - class InvalidFactory < StandardError; end + InvalidFactory = Class.new(StandardError) def initialize(entry) @entry = entry diff --git a/lib/gitlab/ci/config/entry/node.rb b/lib/gitlab/ci/config/entry/node.rb index 5eef2868cd6..55a5447ab51 100644 --- a/lib/gitlab/ci/config/entry/node.rb +++ b/lib/gitlab/ci/config/entry/node.rb @@ -6,7 +6,7 @@ module Gitlab # Base abstract class for each configuration entry node. # class Node - class InvalidError < StandardError; end + InvalidError = Class.new(StandardError) attr_reader :config, :metadata attr_accessor :key, :parent, :description diff --git a/lib/gitlab/ci/config/loader.rb b/lib/gitlab/ci/config/loader.rb index dbf6eb0edbe..e7d9f6a7761 100644 --- a/lib/gitlab/ci/config/loader.rb +++ b/lib/gitlab/ci/config/loader.rb @@ -2,7 +2,7 @@ module Gitlab module Ci class Config class Loader - class FormatError < StandardError; end + FormatError = Class.new(StandardError) def initialize(config) @config = YAML.safe_load(config, [Symbol], [], true) diff --git a/lib/gitlab/conflict/file.rb b/lib/gitlab/conflict/file.rb index d80bc748209..75a213ef752 100644 --- a/lib/gitlab/conflict/file.rb +++ b/lib/gitlab/conflict/file.rb @@ -4,8 +4,7 @@ module Gitlab include Gitlab::Routing.url_helpers include IconsHelper - class MissingResolution < ResolutionError - end + MissingResolution = Class.new(ResolutionError) CONTEXT_LINES = 3 diff --git a/lib/gitlab/conflict/file_collection.rb b/lib/gitlab/conflict/file_collection.rb index fa5bd4649d4..990b719ecfd 100644 --- a/lib/gitlab/conflict/file_collection.rb +++ b/lib/gitlab/conflict/file_collection.rb @@ -1,8 +1,7 @@ module Gitlab module Conflict class FileCollection - class ConflictSideMissing < StandardError - end + ConflictSideMissing = Class.new(StandardError) attr_reader :merge_request, :our_commit, :their_commit diff --git a/lib/gitlab/conflict/parser.rb b/lib/gitlab/conflict/parser.rb index ddd657903fb..d3524c338ee 100644 --- a/lib/gitlab/conflict/parser.rb +++ b/lib/gitlab/conflict/parser.rb @@ -1,25 +1,15 @@ module Gitlab module Conflict class Parser - class UnresolvableError < StandardError - end - - class UnmergeableFile < UnresolvableError - end - - class UnsupportedEncoding < UnresolvableError - end + UnresolvableError = Class.new(StandardError) + UnmergeableFile = Class.new(UnresolvableError) + UnsupportedEncoding = Class.new(UnresolvableError) # Recoverable errors - the conflict can be resolved in an editor, but not with # sections. - class ParserError < StandardError - end - - class UnexpectedDelimiter < ParserError - end - - class MissingEndDelimiter < ParserError - end + ParserError = Class.new(StandardError) + UnexpectedDelimiter = Class.new(ParserError) + MissingEndDelimiter = Class.new(ParserError) def parse(text, our_path:, their_path:, parent_file: nil) raise UnmergeableFile if text.blank? # Typically a binary file diff --git a/lib/gitlab/conflict/resolution_error.rb b/lib/gitlab/conflict/resolution_error.rb index a0f2006bc24..0b61256b35a 100644 --- a/lib/gitlab/conflict/resolution_error.rb +++ b/lib/gitlab/conflict/resolution_error.rb @@ -1,6 +1,5 @@ module Gitlab module Conflict - class ResolutionError < StandardError - end + ResolutionError = Class.new(StandardError) end end diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb index b64db5d01ae..ec0529b5a4b 100644 --- a/lib/gitlab/email/receiver.rb +++ b/lib/gitlab/email/receiver.rb @@ -4,19 +4,19 @@ require_dependency 'gitlab/email/handler' # Inspired in great part by Discourse's Email::Receiver module Gitlab module Email - class ProcessingError < StandardError; end - class EmailUnparsableError < ProcessingError; end - class SentNotificationNotFoundError < ProcessingError; end - class ProjectNotFound < ProcessingError; end - class EmptyEmailError < ProcessingError; end - class AutoGeneratedEmailError < ProcessingError; end - class UserNotFoundError < ProcessingError; end - class UserBlockedError < ProcessingError; end - class UserNotAuthorizedError < ProcessingError; end - class NoteableNotFoundError < ProcessingError; end - class InvalidNoteError < ProcessingError; end - class InvalidIssueError < ProcessingError; end - class UnknownIncomingEmail < ProcessingError; end + ProcessingError = Class.new(StandardError) + EmailUnparsableError = Class.new(ProcessingError) + SentNotificationNotFoundError = Class.new(ProcessingError) + ProjectNotFound = Class.new(ProcessingError) + EmptyEmailError = Class.new(ProcessingError) + AutoGeneratedEmailError = Class.new(ProcessingError) + UserNotFoundError = Class.new(ProcessingError) + UserBlockedError = Class.new(ProcessingError) + UserNotAuthorizedError = Class.new(ProcessingError) + NoteableNotFoundError = Class.new(ProcessingError) + InvalidNoteError = Class.new(ProcessingError) + InvalidIssueError = Class.new(ProcessingError) + UnknownIncomingEmail = Class.new(ProcessingError) class Receiver def initialize(raw) diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb index d6b3b5705a9..2a017c93f57 100644 --- a/lib/gitlab/git/diff.rb +++ b/lib/gitlab/git/diff.rb @@ -2,7 +2,7 @@ module Gitlab module Git class Diff - class TimeoutError < StandardError; end + TimeoutError = Class.new(StandardError) include Gitlab::Git::EncodingHelper # Diff properties diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 8ec90885231..6540730ca7a 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -10,9 +10,9 @@ module Gitlab SEARCH_CONTEXT_LINES = 3 - class NoRepository < StandardError; end - class InvalidBlobName < StandardError; end - class InvalidRef < StandardError; end + NoRepository = Class.new(StandardError) + InvalidBlobName = Class.new(StandardError) + InvalidRef = Class.new(StandardError) # Full path to repo attr_reader :path @@ -199,13 +199,17 @@ module Gitlab nil end + def archive_prefix(ref, sha) + project_name = self.name.chomp('.git') + "#{project_name}-#{ref.parameterize}-#{sha}" + end + def archive_metadata(ref, storage_path, format = "tar.gz") ref ||= root_ref commit = Gitlab::Git::Commit.find(self, ref) return {} if commit.nil? - project_name = self.name.chomp('.git') - prefix = "#{project_name}-#{ref}-#{commit.id}" + prefix = archive_prefix(ref, commit.id) { 'RepoPath' => path, diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb new file mode 100644 index 00000000000..b981a629fb0 --- /dev/null +++ b/lib/gitlab/gitaly_client.rb @@ -0,0 +1,29 @@ +require 'gitaly' + +module Gitlab + module GitalyClient + def self.gitaly_address + if Gitlab.config.gitaly.socket_path + "unix://#{Gitlab.config.gitaly.socket_path}" + end + end + + def self.channel + return @channel if defined?(@channel) + + @channel = + if enabled? + # NOTE: Gitaly currently runs on a Unix socket, so permissions are + # handled using the file system and no additional authentication is + # required (therefore the :this_channel_is_insecure flag) + GRPC::Core::Channel.new(gitaly_address, {}, :this_channel_is_insecure) + else + nil + end + end + + def self.enabled? + gitaly_address.present? + end + end +end diff --git a/lib/gitlab/gitaly_client/notifications.rb b/lib/gitlab/gitaly_client/notifications.rb new file mode 100644 index 00000000000..b827a56207f --- /dev/null +++ b/lib/gitlab/gitaly_client/notifications.rb @@ -0,0 +1,17 @@ +module Gitlab + module GitalyClient + class Notifications + attr_accessor :stub + + def initialize + @stub = Gitaly::Notifications::Stub.new(nil, nil, channel_override: GitalyClient.channel) + end + + def post_receive(repo_path) + repository = Gitaly::Repository.new(path: repo_path) + request = Gitaly::PostReceiveRequest.new(repository: repository) + stub.post_receive(request) + end + end + end +end diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 101b1b80c1e..9c384069661 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -1,7 +1,7 @@ module Gitlab module GonHelper def add_gon_variables - gon.api_version = API::API.version + gon.api_version = 'v3' # v4 Is not officially released yet, therefore can't be considered as "frozen" gon.default_avatar_url = URI.join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s gon.max_file_size = current_application_settings.max_attachment_size gon.relative_url_root = Gitlab.config.gitlab.relative_url_root diff --git a/lib/gitlab/import_export/error.rb b/lib/gitlab/import_export/error.rb index e341c4d9cf8..788eedf2686 100644 --- a/lib/gitlab/import_export/error.rb +++ b/lib/gitlab/import_export/error.rb @@ -1,5 +1,5 @@ module Gitlab module ImportExport - class Error < StandardError; end + Error = Class.new(StandardError) end end diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb index 95d2f559588..fcf51b7fc5b 100644 --- a/lib/gitlab/o_auth/user.rb +++ b/lib/gitlab/o_auth/user.rb @@ -5,7 +5,7 @@ # module Gitlab module OAuth - class SignupDisabledError < StandardError; end + SignupDisabledError = Class.new(StandardError) class User attr_accessor :auth_hash, :gl_user diff --git a/lib/gitlab/route_map.rb b/lib/gitlab/route_map.rb index 72d00abfcc2..36791fae60f 100644 --- a/lib/gitlab/route_map.rb +++ b/lib/gitlab/route_map.rb @@ -1,6 +1,6 @@ module Gitlab class RouteMap - class FormatError < StandardError; end + FormatError = Class.new(StandardError) def initialize(data) begin diff --git a/lib/gitlab/seeder.rb b/lib/gitlab/seeder.rb index b7f825e8284..823f697f51c 100644 --- a/lib/gitlab/seeder.rb +++ b/lib/gitlab/seeder.rb @@ -1,24 +1,23 @@ +module DeliverNever + def deliver_later + self + end +end + module Gitlab class Seeder def self.quiet mute_mailer SeedFu.quiet = true + yield + SeedFu.quiet = false puts "\nOK".color(:green) end - def self.by_user(user) - yield - end - def self.mute_mailer - code = <<-eos -def Notify.deliver_later - self -end - eos - eval(code) # rubocop:disable Security/Eval + ActionMailer::MessageDelivery.prepend(DeliverNever) end end end diff --git a/lib/gitlab/serializer/pagination.rb b/lib/gitlab/serializer/pagination.rb index bf2c0acc729..9c92b83dddc 100644 --- a/lib/gitlab/serializer/pagination.rb +++ b/lib/gitlab/serializer/pagination.rb @@ -1,7 +1,7 @@ module Gitlab module Serializer class Pagination - class InvalidResourceError < StandardError; end + InvalidResourceError = Class.new(StandardError) include ::API::Helpers::Pagination def initialize(request, response) diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index 7374d2bc8b8..da8d8ddb8ed 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -2,7 +2,7 @@ require 'securerandom' module Gitlab class Shell - class Error < StandardError; end + Error = Class.new(StandardError) KeyAdder = Struct.new(:io) do def add_key(id, key) diff --git a/lib/gitlab/template/finders/repo_template_finder.rb b/lib/gitlab/template/finders/repo_template_finder.rb index 22c39436cb2..cb7957e2af9 100644 --- a/lib/gitlab/template/finders/repo_template_finder.rb +++ b/lib/gitlab/template/finders/repo_template_finder.rb @@ -4,7 +4,7 @@ module Gitlab module Finders class RepoTemplateFinder < BaseTemplateFinder # Raised when file is not found - class FileNotFoundError < StandardError; end + FileNotFoundError = Class.new(StandardError) def initialize(project, base_dir, extension, categories = {}) @categories = categories diff --git a/lib/gitlab/update_path_error.rb b/lib/gitlab/update_path_error.rb index ce14cc887d0..8947ecfb92e 100644 --- a/lib/gitlab/update_path_error.rb +++ b/lib/gitlab/update_path_error.rb @@ -1,3 +1,3 @@ module Gitlab - class UpdatePathError < StandardError; end + UpdatePathError = Class.new(StandardError) end diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb index b28708c34e1..2248763c106 100644 --- a/lib/gitlab/visibility_level.rb +++ b/lib/gitlab/visibility_level.rb @@ -35,6 +35,10 @@ module Gitlab class << self delegate :values, to: :options + def string_values + string_options.keys + end + def options { 'Private' => PRIVATE, @@ -43,6 +47,14 @@ module Gitlab } end + def string_options + { + 'private' => PRIVATE, + 'internal' => INTERNAL, + 'public' => PUBLIC + } + end + def highest_allowed_level restricted_levels = current_application_settings.restricted_visibility_levels @@ -82,18 +94,39 @@ module Gitlab level_name end + + def level_value(level) + return string_options[level] if level.is_a? String + level + end + + def string_level(level) + string_options.key(level) + end end def private? - visibility_level_field == PRIVATE + visibility_level_value == PRIVATE end def internal? - visibility_level_field == INTERNAL + visibility_level_value == INTERNAL end def public? - visibility_level_field == PUBLIC + visibility_level_value == PUBLIC + end + + def visibility_level_value + self[visibility_level_field] + end + + def visibility + Gitlab::VisibilityLevel.string_level(visibility_level_value) + end + + def visibility=(level) + self[visibility_level_field] = Gitlab::VisibilityLevel.level_value(level) end end end diff --git a/lib/mattermost/client.rb b/lib/mattermost/client.rb index e55c0d6ac49..ad6df246091 100644 --- a/lib/mattermost/client.rb +++ b/lib/mattermost/client.rb @@ -1,5 +1,5 @@ module Mattermost - class ClientError < Mattermost::Error; end + ClientError = Class.new(Mattermost::Error) class Client attr_reader :user diff --git a/lib/mattermost/error.rb b/lib/mattermost/error.rb index 014df175be0..dee6deb7974 100644 --- a/lib/mattermost/error.rb +++ b/lib/mattermost/error.rb @@ -1,3 +1,3 @@ module Mattermost - class Error < StandardError; end + Error = Class.new(StandardError) end diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb index 377cb7b1021..5388966605d 100644 --- a/lib/mattermost/session.rb +++ b/lib/mattermost/session.rb @@ -5,7 +5,7 @@ module Mattermost end end - class ConnectionError < Mattermost::Error; end + ConnectionError = Class.new(Mattermost::Error) # This class' prime objective is to obtain a session token on a Mattermost # instance with SSO configured where this GitLab instance is the provider. diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index 5661394058d..330031aaddc 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -82,6 +82,9 @@ server { ## # ssl_dhparam /etc/ssl/certs/dhparam.pem; + ## [Optional] Enable HTTP Strict Transport Security + # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains"; + ## Individual nginx logs for this GitLab vhost access_log /var/log/nginx/gitlab_access.log; error_log /var/log/nginx/gitlab_error.log; diff --git a/package.json b/package.json index 5528303ab21..6b2991f9608 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,11 @@ "dropzone": "^4.2.0", "es6-promise": "^4.0.5", "jquery": "^2.2.1", - "jquery-ui": "git+https://github.com/jquery/jquery-ui#1.11.4", "jquery-ujs": "^1.2.1", "js-cookie": "^2.1.3", "mousetrap": "^1.4.6", "pikaday": "^1.5.1", + "raw-loader": "^0.5.1", "select2": "3.5.2-browserify", "stats-webpack-plugin": "^0.4.3", "timeago.js": "^2.0.5", diff --git a/public/ci/build-canceled.svg b/public/ci/build-canceled.svg deleted file mode 100644 index 922e28bf696..00000000000 --- a/public/ci/build-canceled.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="97" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="97" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#9f9f9f" d="M37 0h60v20H37z"/><path fill="url(#b)" d="M0 0h97v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="66" y="15" fill="#010101" fill-opacity=".3">canceled</text><text x="66" y="14">canceled</text></g></svg>
\ No newline at end of file diff --git a/public/ci/build-failed.svg b/public/ci/build-failed.svg deleted file mode 100644 index 1aefd3f1761..00000000000 --- a/public/ci/build-failed.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="78" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="78" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#e05d44" d="M37 0h41v20H37z"/><path fill="url(#b)" d="M0 0h78v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="56.5" y="15" fill="#010101" fill-opacity=".3">failed</text><text x="56.5" y="14">failed</text></g></svg>
\ No newline at end of file diff --git a/public/ci/build-pending.svg b/public/ci/build-pending.svg deleted file mode 100644 index 536931af84d..00000000000 --- a/public/ci/build-pending.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="92" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="92" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#dfb317" d="M37 0h55v20H37z"/><path fill="url(#b)" d="M0 0h92v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="63.5" y="15" fill="#010101" fill-opacity=".3">pending</text><text x="63.5" y="14">pending</text></g></svg>
\ No newline at end of file diff --git a/public/ci/build-running.svg b/public/ci/build-running.svg deleted file mode 100644 index 0d71eef3c34..00000000000 --- a/public/ci/build-running.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="90" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="90" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#dfb317" d="M37 0h53v20H37z"/><path fill="url(#b)" d="M0 0h90v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="62.5" y="15" fill="#010101" fill-opacity=".3">running</text><text x="62.5" y="14">running</text></g></svg>
\ No newline at end of file diff --git a/public/ci/build-skipped.svg b/public/ci/build-skipped.svg deleted file mode 100644 index f15507188e0..00000000000 --- a/public/ci/build-skipped.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="97" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="97" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#9f9f9f" d="M37 0h60v20H37z"/><path fill="url(#b)" d="M0 0h97v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="66" y="15" fill="#010101" fill-opacity=".3">skipped</text><text x="66" y="14">skipped</text></g></svg>
\ No newline at end of file diff --git a/public/ci/build-success.svg b/public/ci/build-success.svg deleted file mode 100644 index 43b67e45f42..00000000000 --- a/public/ci/build-success.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="91" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="91" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#4c1" d="M37 0h54v20H37z"/><path fill="url(#b)" d="M0 0h91v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="63" y="15" fill="#010101" fill-opacity=".3">success</text><text x="63" y="14">success</text></g></svg>
\ No newline at end of file diff --git a/public/ci/build-unknown.svg b/public/ci/build-unknown.svg deleted file mode 100644 index c72a2f5a7f5..00000000000 --- a/public/ci/build-unknown.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="98" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="98" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#9f9f9f" d="M37 0h61v20H37z"/><path fill="url(#b)" d="M0 0h98v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="66.5" y="15" fill="#010101" fill-opacity=".3">unknown</text><text x="66.5" y="14">unknown</text></g></svg>
\ No newline at end of file diff --git a/rubocop/cop/custom_error_class.rb b/rubocop/cop/custom_error_class.rb new file mode 100644 index 00000000000..38d93acfe88 --- /dev/null +++ b/rubocop/cop/custom_error_class.rb @@ -0,0 +1,64 @@ +module RuboCop + module Cop + # This cop makes sure that custom error classes, when empty, are declared + # with Class.new. + # + # @example + # # bad + # class FooError < StandardError + # end + # + # # okish + # class FooError < StandardError; end + # + # # good + # FooError = Class.new(StandardError) + class CustomErrorClass < RuboCop::Cop::Cop + MSG = 'Use `Class.new(SuperClass)` to define an empty custom error class.'.freeze + + def on_class(node) + _klass, parent, body = node.children + + return if body + + parent_klass = class_name_from_node(parent) + + return unless parent_klass && parent_klass.to_s.end_with?('Error') + + add_offense(node, :expression) + end + + def autocorrect(node) + klass, parent, _body = node.children + replacement = "#{class_name_from_node(klass)} = Class.new(#{class_name_from_node(parent)})" + + lambda do |corrector| + corrector.replace(node.source_range, replacement) + end + end + + private + + # The nested constant `Foo::Bar::Baz` looks like: + # + # s(:const, + # s(:const, + # s(:const, nil, :Foo), :Bar), :Baz) + # + # So recurse through that to get the name as written in the source. + # + def class_name_from_node(node, suffix = nil) + return unless node&.type == :const + + name = node.children[1].to_s + name = "#{name}::#{suffix}" if suffix + + if node.children[0] + class_name_from_node(node.children[0], name) + else + name + end + end + end + end +end diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb index d4266d0deae..ea8e0f64b0d 100644 --- a/rubocop/rubocop.rb +++ b/rubocop/rubocop.rb @@ -1,3 +1,4 @@ +require_relative 'cop/custom_error_class' require_relative 'cop/gem_fetcher' require_relative 'cop/migration/add_column' require_relative 'cop/migration/add_column_with_default' diff --git a/spec/controllers/ci/projects_controller_spec.rb b/spec/controllers/ci/projects_controller_spec.rb deleted file mode 100644 index 86f01f437a2..00000000000 --- a/spec/controllers/ci/projects_controller_spec.rb +++ /dev/null @@ -1,74 +0,0 @@ -require 'spec_helper' - -describe Ci::ProjectsController do - let(:visibility) { :public } - let!(:project) { create(:empty_project, visibility, ci_id: 1) } - let(:ci_id) { project.ci_id } - - describe '#index' do - context 'user signed in' do - before do - sign_in(create(:user)) - get(:index) - end - - it 'redirects to /' do - expect(response).to redirect_to(root_path) - end - end - - context 'user not signed in' do - before { get(:index) } - - it 'redirects to sign in page' do - expect(response).to redirect_to(new_user_session_path) - end - end - end - - ## - # Specs for *deprecated* CI badge - # - describe '#badge' do - shared_examples 'badge provider' do - it 'shows badge' do - expect(response.status).to eq 200 - expect(response.headers) - .to include('Content-Type' => 'image/svg+xml') - end - end - - context 'user not signed in' do - before { get(:badge, id: ci_id) } - - context 'project has no ci_id reference' do - let(:ci_id) { 123 } - - it 'returns 404' do - expect(response.status).to eq 404 - end - end - - context 'project is public' do - let(:visibility) { :public } - it_behaves_like 'badge provider' - end - - context 'project is private' do - let(:visibility) { :private } - it_behaves_like 'badge provider' - end - end - - context 'user signed in' do - let(:user) { create(:user) } - before { sign_in(user) } - before { get(:badge, id: ci_id) } - - context 'private is internal' do - let(:visibility) { :internal } - it_behaves_like 'badge provider' - end - end - end -end diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb index 640baa3a01c..b223a22ae60 100644 --- a/spec/controllers/projects/commit_controller_spec.rb +++ b/spec/controllers/projects/commit_controller_spec.rb @@ -166,7 +166,7 @@ describe Projects::CommitController do post(:revert, namespace_id: project.namespace, project_id: project, - target_branch: 'master', + start_branch: 'master', id: commit.id) expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master') @@ -179,7 +179,7 @@ describe Projects::CommitController do post(:revert, namespace_id: project.namespace, project_id: project, - target_branch: 'master', + start_branch: 'master', id: commit.id) end @@ -188,7 +188,7 @@ describe Projects::CommitController do post(:revert, namespace_id: project.namespace, project_id: project, - target_branch: 'master', + start_branch: 'master', id: commit.id) expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, commit.id) @@ -215,7 +215,7 @@ describe Projects::CommitController do post(:cherry_pick, namespace_id: project.namespace, project_id: project, - target_branch: 'master', + start_branch: 'master', id: master_pickable_commit.id) expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master') @@ -228,7 +228,7 @@ describe Projects::CommitController do post(:cherry_pick, namespace_id: project.namespace, project_id: project, - target_branch: 'master', + start_branch: 'master', id: master_pickable_commit.id) end @@ -237,7 +237,7 @@ describe Projects::CommitController do post(:cherry_pick, namespace_id: project.namespace, project_id: project, - target_branch: 'master', + start_branch: 'master', id: master_pickable_commit.id) expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, master_pickable_commit.id) diff --git a/spec/controllers/projects/graphs_controller_spec.rb b/spec/controllers/projects/graphs_controller_spec.rb index c4a7aa7d63e..049bae1899d 100644 --- a/spec/controllers/projects/graphs_controller_spec.rb +++ b/spec/controllers/projects/graphs_controller_spec.rb @@ -9,7 +9,23 @@ describe Projects::GraphsController do project.team << [user, :master] end - describe 'GET #languages' do + describe 'GET languages' do + it "redirects_to action charts" do + get(:commits, namespace_id: project.namespace.path, project_id: project.path, id: 'master') + + expect(response).to redirect_to action: :charts + end + end + + describe 'GET commits' do + it "redirects_to action charts" do + get(:commits, namespace_id: project.namespace.path, project_id: project.path, id: 'master') + + expect(response).to redirect_to action: :charts + end + end + + describe 'GET charts' do let(:linguist_repository) do double(languages: { 'Ruby' => 1000, @@ -34,7 +50,7 @@ describe Projects::GraphsController do end it 'sets the correct colour according to language' do - get(:languages, namespace_id: project.namespace, project_id: project, id: 'master') + get(:charts, namespace_id: project.namespace, project_id: project, id: 'master') expected_values.each do |val| expect(assigns(:languages)).to include(a_hash_including(val)) diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 1ced666bb36..250d64f7055 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -43,7 +43,8 @@ describe Projects::MergeRequestsController do submit_new_merge_request(format: :json) expect(response).to be_ok - expect(json_response).not_to be_empty + expect(json_response).to have_key 'pipelines' + expect(json_response['pipelines']).not_to be_empty end end end @@ -319,41 +320,41 @@ describe Projects::MergeRequestsController do merge_with_sha end - context 'when merge_when_build_succeeds is passed' do - def merge_when_build_succeeds - post :merge, base_params.merge(sha: merge_request.diff_head_sha, merge_when_build_succeeds: '1') + context 'when the pipeline succeeds is passed' do + def merge_when_pipeline_succeeds + post :merge, base_params.merge(sha: merge_request.diff_head_sha, merge_when_pipeline_succeeds: '1') end before do create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch) end - it 'returns :merge_when_build_succeeds' do - merge_when_build_succeeds + it 'returns :merge_when_pipeline_succeeds' do + merge_when_pipeline_succeeds - expect(assigns(:status)).to eq(:merge_when_build_succeeds) + expect(assigns(:status)).to eq(:merge_when_pipeline_succeeds) end - it 'sets the MR to merge when the build succeeds' do - service = double(:merge_when_build_succeeds_service) + it 'sets the MR to merge when the pipeline succeeds' do + service = double(:merge_when_pipeline_succeeds_service) expect(MergeRequests::MergeWhenPipelineSucceedsService) .to receive(:new).with(project, anything, anything) .and_return(service) expect(service).to receive(:execute).with(merge_request) - merge_when_build_succeeds + merge_when_pipeline_succeeds end - context 'when project.only_allow_merge_if_build_succeeds? is true' do + context 'when project.only_allow_merge_if_pipeline_succeeds? is true' do before do - project.update_column(:only_allow_merge_if_build_succeeds, true) + project.update_column(:only_allow_merge_if_pipeline_succeeds, true) end - it 'returns :merge_when_build_succeeds' do - merge_when_build_succeeds + it 'returns :merge_when_pipeline_succeeds' do + merge_when_pipeline_succeeds - expect(assigns(:status)).to eq(:merge_when_build_succeeds) + expect(assigns(:status)).to eq(:merge_when_pipeline_succeeds) end end end @@ -1134,14 +1135,14 @@ describe Projects::MergeRequestsController do end context 'when waiting for build' do - let(:merge_request) { create(:merge_request, source_project: project, merge_when_build_succeeds: true, merge_user: user) } + let(:merge_request) { create(:merge_request, source_project: project, merge_when_pipeline_succeeds: true, merge_user: user) } it 'returns an OK response' do expect(response).to have_http_status(:ok) end - it 'sets status to :merge_when_build_succeeds' do - expect(assigns(:status)).to eq(:merge_when_build_succeeds) + it 'sets status to :merge_when_pipeline_succeeds' do + expect(assigns(:status)).to eq(:merge_when_pipeline_succeeds) expect(response).to render_template('merge') end end diff --git a/spec/controllers/root_controller_spec.rb b/spec/controllers/root_controller_spec.rb index b14d275f7fa..b32eb39b1fb 100644 --- a/spec/controllers/root_controller_spec.rb +++ b/spec/controllers/root_controller_spec.rb @@ -2,6 +2,26 @@ require 'spec_helper' describe RootController do describe 'GET index' do + context 'when user is not logged in' do + it 'redirects to the sign-in page' do + get :index + + expect(response).to redirect_to(new_user_session_path) + end + + context 'when a custom home page URL is defined' do + before do + stub_application_setting(home_page_url: 'https://gitlab.com') + end + + it 'redirects the user to the custom home page URL' do + get :index + + expect(response).to redirect_to('https://gitlab.com') + end + end + end + context 'with a user' do let(:user) { create(:user) } @@ -12,55 +32,60 @@ describe RootController do context 'who has customized their dashboard setting for starred projects' do before do - user.update_attribute(:dashboard, 'stars') + user.dashboard = 'stars' end it 'redirects to their specified dashboard' do get :index + expect(response).to redirect_to starred_dashboard_projects_path end end context 'who has customized their dashboard setting for project activities' do before do - user.update_attribute(:dashboard, 'project_activity') + user.dashboard = 'project_activity' end it 'redirects to the activity list' do get :index + expect(response).to redirect_to activity_dashboard_path end end context 'who has customized their dashboard setting for starred project activities' do before do - user.update_attribute(:dashboard, 'starred_project_activity') + user.dashboard = 'starred_project_activity' end it 'redirects to the activity list' do get :index + expect(response).to redirect_to activity_dashboard_path(filter: 'starred') end end context 'who has customized their dashboard setting for groups' do before do - user.update_attribute(:dashboard, 'groups') + user.dashboard = 'groups' end it 'redirects to their group list' do get :index + expect(response).to redirect_to dashboard_groups_path end end context 'who has customized their dashboard setting for todos' do before do - user.update_attribute(:dashboard, 'todos') + user.dashboard = 'todos' end it 'redirects to their todo list' do get :index + expect(response).to redirect_to dashboard_todos_path end end @@ -68,6 +93,7 @@ describe RootController do context 'who uses the default dashboard setting' do it 'renders the default dashboard' do get :index + expect(response).to render_template 'dashboard/projects/index' end end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index a90534d10ba..cabe128acf7 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -76,6 +76,18 @@ FactoryGirl.define do manual end + trait :tags do + tag_list [:docker, :ruby] + end + + trait :on_tag do + tag true + end + + trait :triggered do + trigger_request factory: :ci_trigger_request_with_variables + end + after(:build) do |build, evaluator| build.project = build.pipeline.project end diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index 22f84150bb3..ae0bbbd6aeb 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -59,8 +59,8 @@ FactoryGirl.define do target_branch "master" end - trait :merge_when_build_succeeds do - merge_when_build_succeeds true + trait :merge_when_pipeline_succeeds do + merge_when_pipeline_succeeds true merge_user author end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 586efdefdb3..04de3512125 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -39,6 +39,10 @@ FactoryGirl.define do trait :empty_repo do after(:create) do |project| project.create_repository + + # We delete hooks so that gitlab-shell will not try to authenticate with + # an API that isn't running + FileUtils.rm_r(File.join(project.repository_storage_path, "#{project.path_with_namespace}.git", 'hooks')) end end diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb index a5265f1b189..c1ac3bb84ad 100644 --- a/spec/factories/todos.rb +++ b/spec/factories/todos.rb @@ -18,11 +18,6 @@ FactoryGirl.define do action { Todo::DIRECTLY_ADDRESSED } end - trait :on_commit do - commit_id RepoHelpers.sample_commit.id - target_type "Commit" - end - trait :build_failed do action { Todo::BUILD_FAILED } target factory: :merge_request @@ -48,4 +43,13 @@ FactoryGirl.define do state :done end end + + factory :on_commit_todo, class: Todo do + project factory: :empty_project + author + user + action { Todo::ASSIGNED } + commit_id RepoHelpers.sample_commit.id + target_type "Commit" + end end diff --git a/spec/features/dashboard/activity_spec.rb b/spec/features/dashboard/activity_spec.rb new file mode 100644 index 00000000000..c977f266296 --- /dev/null +++ b/spec/features/dashboard/activity_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +RSpec.describe 'Dashboard Activity', feature: true do + before do + login_as(create :user) + visit activity_dashboard_path + end + + it_behaves_like "it has an RSS button with current_user's private token" + it_behaves_like "an autodiscoverable RSS feed with current_user's private token" +end diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb new file mode 100644 index 00000000000..ca04107d33a --- /dev/null +++ b/spec/features/dashboard/groups_list_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe 'Dashboard Groups page', js: true, feature: true do + include WaitForAjax + + let!(:user) { create :user } + let!(:group) { create(:group) } + let!(:nested_group) { create(:group, :nested) } + let!(:another_group) { create(:group) } + + before do + group.add_owner(user) + nested_group.add_owner(user) + + login_as(user) + + visit dashboard_groups_path + end + + it 'shows groups user is member of' do + expect(page).to have_content(group.full_name) + expect(page).to have_content(nested_group.full_name) + expect(page).not_to have_content(another_group.full_name) + end + + it 'filters groups' do + fill_in 'filter_groups', with: group.name + wait_for_ajax + + expect(page).to have_content(group.full_name) + expect(page).not_to have_content(nested_group.full_name) + expect(page).not_to have_content(another_group.full_name) + end + + it 'resets search when user cleans the input' do + fill_in 'filter_groups', with: group.name + wait_for_ajax + + fill_in 'filter_groups', with: "" + wait_for_ajax + + expect(page).to have_content(group.full_name) + expect(page).to have_content(nested_group.full_name) + expect(page).not_to have_content(another_group.full_name) + expect(page.all('.js-groups-list-holder .content-list li').length).to eq 2 + end +end diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb index 2db1cf71209..f4420814c3a 100644 --- a/spec/features/dashboard/issues_spec.rb +++ b/spec/features/dashboard/issues_spec.rb @@ -45,4 +45,7 @@ RSpec.describe 'Dashboard Issues', feature: true do expect(page).to have_content(assigned_issue.title) expect(page).to have_content(other_issue.title) end + + it_behaves_like "it has an RSS button with current_user's private token" + it_behaves_like "an autodiscoverable RSS feed with current_user's private token" end diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb new file mode 100644 index 00000000000..63eb5c697c2 --- /dev/null +++ b/spec/features/dashboard/projects_spec.rb @@ -0,0 +1,10 @@ +require 'spec_helper' + +RSpec.describe 'Dashboard Projects', feature: true do + before do + login_as(create :user) + visit dashboard_projects_path + end + + it_behaves_like "an autodiscoverable RSS feed with current_user's private token" +end diff --git a/spec/features/explore/groups_list_spec.rb b/spec/features/explore/groups_list_spec.rb new file mode 100644 index 00000000000..773ae4b38bc --- /dev/null +++ b/spec/features/explore/groups_list_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe 'Explore Groups page', js: true, feature: true do + include WaitForAjax + + let!(:user) { create :user } + let!(:group) { create(:group) } + let!(:public_group) { create(:group, :public) } + let!(:private_group) { create(:group, :private) } + + before do + group.add_owner(user) + + login_as(user) + + visit explore_groups_path + end + + it 'shows groups user is member of' do + expect(page).to have_content(group.full_name) + expect(page).to have_content(public_group.full_name) + expect(page).not_to have_content(private_group.full_name) + end + + it 'filters groups' do + fill_in 'filter_groups', with: group.name + wait_for_ajax + + expect(page).to have_content(group.full_name) + expect(page).not_to have_content(public_group.full_name) + expect(page).not_to have_content(private_group.full_name) + end + + it 'resets search when user cleans the input' do + fill_in 'filter_groups', with: group.name + wait_for_ajax + + fill_in 'filter_groups', with: "" + wait_for_ajax + + expect(page).to have_content(group.full_name) + expect(page).to have_content(public_group.full_name) + expect(page).not_to have_content(private_group.full_name) + expect(page.all('.js-groups-list-holder .content-list li').length).to eq 2 + end +end diff --git a/spec/features/groups/activity_spec.rb b/spec/features/groups/activity_spec.rb new file mode 100644 index 00000000000..3b481cba424 --- /dev/null +++ b/spec/features/groups/activity_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +feature 'Group activity page', feature: true do + let(:group) { create(:group) } + let(:path) { activity_group_path(group) } + + context 'when signed in' do + before do + user = create(:group_member, :developer, user: create(:user), group: group ).user + login_as(user) + visit path + end + + it_behaves_like "it has an RSS button with current_user's private token" + it_behaves_like "an autodiscoverable RSS feed with current_user's private token" + end + + context 'when signed out' do + before do + visit path + end + + it_behaves_like "it has an RSS button without a private token" + it_behaves_like "an autodiscoverable RSS feed without a private token" + end +end diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb index 476eca17a9d..1b3747c390b 100644 --- a/spec/features/groups/issues_spec.rb +++ b/spec/features/groups/issues_spec.rb @@ -5,4 +5,22 @@ feature 'Group issues page', feature: true do let(:issuable) { create(:issue, project: project, title: "this is my created issuable")} include_examples 'project features apply to issuables', Issue + + context 'rss feed' 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 private token" + it_behaves_like "an autodiscoverable RSS feed with current_user's private token" + end + + context 'when signed out' do + let(:user) { nil } + + it_behaves_like "it has an RSS button without a private token" + it_behaves_like "an autodiscoverable RSS feed without a private token" + end + end end diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb new file mode 100644 index 00000000000..fb39693e8ca --- /dev/null +++ b/spec/features/groups/show_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +feature 'Group show page', feature: true do + let(:group) { create(:group) } + let(:path) { group_path(group) } + + context 'when signed in' do + before do + user = create(:group_member, :developer, user: create(:user), group: group ).user + login_as(user) + visit path + end + + it_behaves_like "an autodiscoverable RSS feed with current_user's private token" + end + + context 'when signed out' do + before do + visit path + end + + it_behaves_like "an autodiscoverable RSS feed without a private token" + end +end diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb index 73e43316dc7..3ab3d2d4229 100644 --- a/spec/features/issues/award_emoji_spec.rb +++ b/spec/features/issues/award_emoji_spec.rb @@ -67,6 +67,18 @@ describe 'Awards Emoji', feature: true do expect(page).not_to have_selector(emoji_counter) end end + + context 'execute /award slash command' do + it 'toggles the emoji award on noteable', js: true do + execute_slash_command('/award :100:') + + expect(find(noteable_award_counter)).to have_text("1") + + execute_slash_command('/award :100:') + + expect(page).not_to have_selector(noteable_award_counter) + end + end end end @@ -80,6 +92,15 @@ describe 'Awards Emoji', feature: true do end end + def execute_slash_command(cmd) + within('.js-main-target-form') do + fill_in 'note[note]', with: cmd + click_button 'Comment' + end + + wait_for_ajax + end + def thumbsup_emoji page.all(emoji_counter).first end @@ -92,6 +113,10 @@ describe 'Awards Emoji', feature: true do 'span.js-counter' end + def noteable_award_counter + ".awards .active" + end + def toggle_smiley_emoji(status) within('.note') do find('.note-emoji-button').click diff --git a/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb b/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb index f2f8f11ab28..0ceaf7bc830 100644 --- a/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb +++ b/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb @@ -34,7 +34,7 @@ feature 'Merge immediately', :feature, :js do click_link 'Merge Immediately' - expect(find('.js-merge-button')).to have_content('Merge in progress') + expect(find('.js-merge-when-pipeline-succeeds-button')).to have_content('Merge in progress') end end end diff --git a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb index 2ea9c317bd1..ed7193b9777 100644 --- a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb +++ b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb @@ -75,7 +75,7 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do context 'when it was enabled and then canceled' do let(:merge_request) do create(:merge_request_with_diffs, - :merge_when_build_succeeds, + :merge_when_pipeline_succeeds, source_project: project, title: 'Bug NS-04', author: user, @@ -97,7 +97,7 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do author: user, merge_user: user, title: 'MepMep', - merge_when_build_succeeds: true) + merge_when_pipeline_succeeds: true) end let!(:build) do diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb index d2f5c4afc93..447764566e0 100644 --- a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb +++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Only allow merge requests to be merged if the build succeeds', feature: true do +feature 'Only allow merge requests to be merged if the pipeline succeeds', feature: true do let(:merge_request) { create(:merge_request_with_diffs) } let(:project) { merge_request.target_project } @@ -27,9 +27,9 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature: status: status) end - context 'when merge requests can only be merged if the build succeeds' do + context 'when merge requests can only be merged if the pipeline succeeds' do before do - project.update_attribute(:only_allow_merge_if_build_succeeds, true) + project.update_attribute(:only_allow_merge_if_pipeline_succeeds, true) end context 'when CI is running' do @@ -88,7 +88,7 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature: context 'when merge requests can be merged when the build failed' do before do - project.update_attribute(:only_allow_merge_if_build_succeeds, false) + project.update_attribute(:only_allow_merge_if_pipeline_succeeds, false) end context 'when CI is running' do diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb index 4ad944366c8..b575aeff0d8 100644 --- a/spec/features/merge_requests/widget_spec.rb +++ b/spec/features/merge_requests/widget_spec.rb @@ -3,8 +3,8 @@ require 'rails_helper' describe 'Merge request', :feature, :js do include WaitForAjax - let(:project) { create(:project) } let(:user) { create(:user) } + let(:project) { create(:project) } let(:merge_request) { create(:merge_request, source_project: project) } before do @@ -31,7 +31,7 @@ describe 'Merge request', :feature, :js do wait_for_ajax - expect(page).to have_selector('.accept_merge_request') + expect(page).to have_selector('.accept-merge-request') end end @@ -51,6 +51,69 @@ describe 'Merge request', :feature, :js do expect(find('.js-environment-link')[:href]).to include(environment.formatted_external_url) end end + + it 'shows green accept merge request button' do + # Wait for the `ci_status` and `merge_check` requests + wait_for_ajax + expect(page).to have_selector('.accept-merge-request.btn-create') + end + end + + context 'view merge request with external CI service' do + before do + create(:service, project: project, + active: true, + type: 'CiService', + category: 'ci') + + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'has danger button while waiting for external CI status' do + # Wait for the `ci_status` and `merge_check` requests + wait_for_ajax + expect(page).to have_selector('.accept-merge-request.btn-danger') + end + end + + context 'view merge request with failed GitLab CI pipelines' do + before do + commit_status = create(:commit_status, project: project, status: 'failed') + pipeline = create(:ci_pipeline, project: project, + sha: merge_request.diff_head_sha, + ref: merge_request.source_branch, + status: 'failed', + statuses: [commit_status]) + create(:ci_build, :pending, pipeline: pipeline) + + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'has danger button when not succeeded' do + # Wait for the `ci_status` and `merge_check` requests + wait_for_ajax + expect(page).to have_selector('.accept-merge-request.btn-danger') + end + end + + context 'view merge request with MWBS button' do + before do + commit_status = create(:commit_status, project: project, status: 'pending') + pipeline = create(:ci_pipeline, project: project, + sha: merge_request.diff_head_sha, + ref: merge_request.source_branch, + status: 'pending', + statuses: [commit_status]) + create(:ci_build, :pending, pipeline: pipeline) + + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'has info button when MWBS button' do + # Wait for the `ci_status` and `merge_check` requests + wait_for_ajax + expect(page).to have_selector('.merge-when-pipeline-succeeds.btn-info') + end end context 'merge error' do diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb index a2e40546588..c3297de709a 100644 --- a/spec/features/milestone_spec.rb +++ b/spec/features/milestone_spec.rb @@ -24,7 +24,7 @@ feature 'Milestone', feature: true do find('input[name="commit"]').click expect(find('.alert-success')).to have_content('Assign some issues to this milestone.') - expect(page).to have_content('Nov 16, 2016 - Dec 16, 2016') + expect(page).to have_content('Nov 16, 2016–Dec 16, 2016') end end diff --git a/spec/features/projects/activity/rss_spec.rb b/spec/features/projects/activity/rss_spec.rb new file mode 100644 index 00000000000..b47c6d431eb --- /dev/null +++ b/spec/features/projects/activity/rss_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +feature 'Project Activity RSS' do + let(:project) { create(:empty_project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } + let(:path) { activity_namespace_project_path(project.namespace, project) } + + before do + create(:issue, project: project) + end + + context 'when signed in' do + before do + user = create(:user) + project.team << [user, :developer] + login_as(user) + visit path + end + + it_behaves_like "it has an RSS button with current_user's private token" + end + + context 'when signed out' do + before do + visit path + end + + it_behaves_like "it has an RSS button without a private token" + end +end diff --git a/spec/features/projects/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb index 7baf7913424..0b972d2a439 100644 --- a/spec/features/projects/commit/cherry_pick_spec.rb +++ b/spec/features/projects/commit/cherry_pick_spec.rb @@ -60,6 +60,7 @@ describe 'Cherry-pick Commits' do click_button 'Cherry-pick' end expect(page).to have_content('The commit has been successfully cherry-picked. You can now submit a merge request to get this change into the original branch.') + expect(page).to have_content("From cherry-pick-#{master_pickable_commit.short_id} into master") end end diff --git a/spec/features/projects/commit/rss_spec.rb b/spec/features/projects/commit/rss_spec.rb new file mode 100644 index 00000000000..6e0e1916f87 --- /dev/null +++ b/spec/features/projects/commit/rss_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +feature 'Project Commits RSS' do + let(:project) { create(:project, :repository, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } + let(:path) { namespace_project_commits_path(project.namespace, project, :master) } + + context 'when signed in' do + before do + user = create(:user) + project.team << [user, :developer] + login_as(user) + visit path + end + + it_behaves_like "it has an RSS button with current_user's private token" + it_behaves_like "an autodiscoverable RSS feed with current_user's private token" + end + + context 'when signed out' do + before do + visit path + end + + it_behaves_like "it has an RSS button without a private token" + it_behaves_like "an autodiscoverable RSS feed without a private token" + end +end diff --git a/spec/features/projects/guest_navigation_menu_spec.rb b/spec/features/projects/guest_navigation_menu_spec.rb index 8120a51c515..726469daba4 100644 --- a/spec/features/projects/guest_navigation_menu_spec.rb +++ b/spec/features/projects/guest_navigation_menu_spec.rb @@ -15,13 +15,11 @@ describe "Guest navigation menu" do within(".nav-links") do expect(page).to have_content 'Project' - expect(page).to have_content 'Activity' expect(page).to have_content 'Issues' expect(page).to have_content 'Wiki' expect(page).not_to have_content 'Repository' expect(page).not_to have_content 'Pipelines' - expect(page).not_to have_content 'Graphs' expect(page).not_to have_content 'Merge Requests' end end diff --git a/spec/features/projects/issues/rss_spec.rb b/spec/features/projects/issues/rss_spec.rb new file mode 100644 index 00000000000..71429f00095 --- /dev/null +++ b/spec/features/projects/issues/rss_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +feature 'Project Issues RSS' do + let(:project) { create(:empty_project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } + let(:path) { namespace_project_issues_path(project.namespace, project) } + + before do + create(:issue, project: project) + end + + context 'when signed in' do + before do + user = create(:user) + project.team << [user, :developer] + login_as(user) + visit path + end + + it_behaves_like "it has an RSS button with current_user's private token" + it_behaves_like "an autodiscoverable RSS feed with current_user's private token" + end + + context 'when signed out' do + before do + visit path + end + + it_behaves_like "it has an RSS button without a private token" + it_behaves_like "an autodiscoverable RSS feed without a private token" + end +end diff --git a/spec/features/projects/main/rss_spec.rb b/spec/features/projects/main/rss_spec.rb new file mode 100644 index 00000000000..b1a3af612a1 --- /dev/null +++ b/spec/features/projects/main/rss_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +feature 'Project RSS' do + let(:project) { create(:project, :repository, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } + let(:path) { namespace_project_path(project.namespace, project) } + + context 'when signed in' do + before do + user = create(:user) + project.team << [user, :developer] + login_as(user) + visit path + end + + it_behaves_like "an autodiscoverable RSS feed with current_user's private token" + end + + context 'when signed out' do + before do + visit path + end + + it_behaves_like "an autodiscoverable RSS feed without a private token" + end +end diff --git a/spec/features/projects/milestones/milestone_spec.rb b/spec/features/projects/milestones/milestone_spec.rb new file mode 100644 index 00000000000..df229d0aa78 --- /dev/null +++ b/spec/features/projects/milestones/milestone_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +feature 'Project milestone', :feature do + let(:user) { create(:user) } + let(:project) { create(:empty_project, name: 'test', namespace: user.namespace) } + let(:milestone) { create(:milestone, project: project) } + + before do + login_as(user) + end + + context 'when project has enabled issues' do + before do + visit namespace_project_milestone_path(project.namespace, project, milestone) + end + + 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' + end + end + + it 'shows issues stats' do + expect(page).to have_content 'issues:' + end + + it 'shows Browse Issues button' do + within('#content-body') do + expect(page).to have_link 'Browse Issues' + end + end + end + + context 'when project has disabled issues' do + before do + project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) + visit namespace_project_milestone_path(project.namespace, project, milestone) + end + + 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' + end + end + + it 'hides issues stats' do + expect(page).to have_no_content 'issues:' + end + + it 'hides Browse Issues button' do + within('#content-body') do + expect(page).not_to have_link 'Browse Issues' + end + end + + it 'does not show an informative message' do + expect(page).not_to have_content('Assign some issues to this milestone.') + end + end +end diff --git a/spec/features/projects/tree/rss_spec.rb b/spec/features/projects/tree/rss_spec.rb new file mode 100644 index 00000000000..9ac51997d65 --- /dev/null +++ b/spec/features/projects/tree/rss_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +feature 'Project Tree RSS' do + let(:project) { create(:project, :repository, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } + let(:path) { namespace_project_tree_path(project.namespace, project, :master) } + + context 'when signed in' do + before do + user = create(:user) + project.team << [user, :developer] + login_as(user) + visit path + end + + it_behaves_like "an autodiscoverable RSS feed with current_user's private token" + end + + context 'when signed out' do + before do + visit path + end + + it_behaves_like "an autodiscoverable RSS feed without a private token" + end +end diff --git a/spec/features/users/rss_spec.rb b/spec/features/users/rss_spec.rb new file mode 100644 index 00000000000..14564abb16d --- /dev/null +++ b/spec/features/users/rss_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +feature 'User RSS' do + let(:path) { user_path(create(:user)) } + + context 'when signed in' do + before do + login_as(create(:user)) + visit path + end + + it_behaves_like "it has an RSS button with current_user's private token" + end + + context 'when signed out' do + before do + visit path + end + + it_behaves_like "it has an RSS button without a private token" + end +end diff --git a/spec/helpers/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb index 14a95479339..68b20a1e4fc 100644 --- a/spec/helpers/milestones_helper_spec.rb +++ b/spec/helpers/milestones_helper_spec.rb @@ -17,7 +17,7 @@ describe MilestonesHelper do it { expect(result_for(due_date: yesterday)).to eq("expired on #{yesterday_formatted}") } it { expect(result_for(start_date: tomorrow)).to eq("starts on #{tomorrow_formatted}") } it { expect(result_for(start_date: yesterday)).to eq("started on #{yesterday_formatted}") } - it { expect(result_for(start_date: yesterday, due_date: tomorrow)).to eq("#{yesterday_formatted} - #{tomorrow_formatted}") } + it { expect(result_for(start_date: yesterday, due_date: tomorrow)).to eq("#{yesterday_formatted}–#{tomorrow_formatted}") } end describe '#milestone_counts' do diff --git a/spec/helpers/rss_helper_spec.rb b/spec/helpers/rss_helper_spec.rb new file mode 100644 index 00000000000..f3f174f3d14 --- /dev/null +++ b/spec/helpers/rss_helper_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe RssHelper do + describe '#rss_url_options' do + context 'when signed in' do + it "includes the current_user's private_token" do + current_user = create(:user) + allow(helper).to receive(:current_user).and_return(current_user) + expect(helper.rss_url_options).to include private_token: current_user.private_token + end + end + + context 'when signed out' do + it "does not have a private_token" do + allow(helper).to receive(:current_user).and_return(nil) + expect(helper.rss_url_options[:private_token]).to be_nil + end + end + end +end diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js index 192916fbc6a..be31f644e20 100644 --- a/spec/javascripts/boards/board_card_spec.js +++ b/spec/javascripts/boards/board_card_spec.js @@ -8,7 +8,7 @@ require('~/boards/models/list'); require('~/boards/models/label'); require('~/boards/stores/boards_store'); -const boardCard = require('~/boards/components/board_card'); +const boardCard = require('~/boards/components/board_card').default; require('./mock_data'); describe('Issue card', () => { diff --git a/spec/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js new file mode 100644 index 00000000000..22c9f12951b --- /dev/null +++ b/spec/javascripts/boards/board_new_issue_spec.js @@ -0,0 +1,191 @@ +/* global boardsMockInterceptor */ +/* global BoardService */ +/* global List */ +/* global listObj */ + +import Vue from 'vue'; +import boardNewIssue from '~/boards/components/board_new_issue'; + +require('~/boards/models/list'); +require('./mock_data'); +require('es6-promise').polyfill(); + +describe('Issue boards new issue form', () => { + let vm; + let list; + const promiseReturn = { + json() { + return { + iid: 100, + }; + }, + }; + const submitIssue = () => { + vm.$el.querySelector('.btn-success').click(); + }; + + beforeEach((done) => { + const BoardNewIssueComp = Vue.extend(boardNewIssue); + + Vue.http.interceptors.push(boardsMockInterceptor); + gl.boardService = new BoardService('/test/issue-boards/board', '', '1'); + gl.issueBoards.BoardsStore.create(); + gl.IssueBoardsApp = new Vue(); + + setTimeout(() => { + list = new List(listObj); + + spyOn(gl.boardService, 'newIssue').and.callFake(() => new Promise((resolve, reject) => { + if (vm.title === 'error') { + reject(); + } else { + resolve(promiseReturn); + } + })); + + vm = new BoardNewIssueComp({ + propsData: { + list, + }, + }).$mount(); + + done(); + }, 0); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor); + }); + + it('disables submit button if title is empty', () => { + expect(vm.$el.querySelector('.btn-success').disabled).toBe(true); + }); + + it('enables submit button if title is not empty', (done) => { + vm.title = 'Testing Title'; + + setTimeout(() => { + expect(vm.$el.querySelector('.form-control').value).toBe('Testing Title'); + expect(vm.$el.querySelector('.btn-success').disabled).not.toBe(true); + + done(); + }, 0); + }); + + it('clears title after clicking cancel', (done) => { + vm.$el.querySelector('.btn-default').click(); + + setTimeout(() => { + expect(vm.title).toBe(''); + done(); + }, 0); + }); + + it('does not create new issue if title is empty', (done) => { + submitIssue(); + + setTimeout(() => { + expect(gl.boardService.newIssue).not.toHaveBeenCalled(); + done(); + }, 0); + }); + + describe('submit success', () => { + it('creates new issue', (done) => { + vm.title = 'submit title'; + + setTimeout(() => { + submitIssue(); + + expect(gl.boardService.newIssue).toHaveBeenCalled(); + done(); + }, 0); + }); + + it('enables button after submit', (done) => { + vm.title = 'submit issue'; + + setTimeout(() => { + submitIssue(); + + expect(vm.$el.querySelector('.btn-success').disbled).not.toBe(true); + done(); + }, 0); + }); + + it('clears title after submit', (done) => { + vm.title = 'submit issue'; + + setTimeout(() => { + submitIssue(); + + expect(vm.title).toBe(''); + done(); + }, 0); + }); + + it('adds new issue to list after submit', (done) => { + vm.title = 'submit issue'; + + setTimeout(() => { + submitIssue(); + + expect(list.issues.length).toBe(2); + expect(list.issues[1].title).toBe('submit issue'); + expect(list.issues[1].subscribed).toBe(true); + done(); + }, 0); + }); + + it('sets detail issue after submit', (done) => { + vm.title = 'submit issue'; + + setTimeout(() => { + submitIssue(); + + expect(gl.issueBoards.BoardsStore.detail.issue.title).toBe('submit issue'); + done(); + }); + }); + + it('sets detail list after submit', (done) => { + vm.title = 'submit issue'; + + setTimeout(() => { + submitIssue(); + + expect(gl.issueBoards.BoardsStore.detail.list.id).toBe(list.id); + done(); + }, 0); + }); + }); + + describe('submit error', () => { + it('removes issue', (done) => { + vm.title = 'error'; + + setTimeout(() => { + submitIssue(); + + setTimeout(() => { + expect(list.issues.length).toBe(1); + done(); + }, 500); + }, 0); + }); + + it('shows error', (done) => { + vm.title = 'error'; + submitIssue(); + + setTimeout(() => { + submitIssue(); + + setTimeout(() => { + expect(vm.error).toBe(true); + done(); + }, 500); + }, 0); + }); + }); +}); diff --git a/spec/javascripts/environments/environment_actions_spec.js.es6 b/spec/javascripts/environments/environment_actions_spec.js.es6 index 850586f9f3a..d50d45d295e 100644 --- a/spec/javascripts/environments/environment_actions_spec.js.es6 +++ b/spec/javascripts/environments/environment_actions_spec.js.es6 @@ -23,7 +23,6 @@ describe('Actions Component', () => { el: document.querySelector('.test-dom-element'), propsData: { actions: actionsMock, - playIconSvg: '<svg></svg>', }, }); @@ -34,33 +33,4 @@ describe('Actions Component', () => { component.$el.querySelector('.dropdown-menu li a').getAttribute('href'), ).toEqual(actionsMock[0].play_path); }); - - it('should render a dropdown with the provided svg', () => { - const actionsMock = [ - { - name: 'bar', - play_path: 'https://gitlab.com/play', - }, - { - name: 'foo', - play_path: '#', - }, - ]; - - const component = new ActionsComponent({ - el: document.querySelector('.test-dom-element'), - propsData: { - actions: actionsMock, - playIconSvg: '<svg></svg>', - }, - }); - - expect( - component.$el.querySelector('.js-dropdown-play-icon-container').children, - ).toContain('svg'); - - expect( - component.$el.querySelector('.js-action-play-icon-container').children, - ).toContain('svg'); - }); }); diff --git a/spec/javascripts/fixtures/projects.json b/spec/javascripts/fixtures/projects.json index 4ce7f5c601a..1339ee00870 100644 --- a/spec/javascripts/fixtures/projects.json +++ b/spec/javascripts/fixtures/projects.json @@ -43,7 +43,7 @@ "avatar_url": null, "star_count": 0, "forks_count": 0, - "only_allow_merge_if_build_succeeds": false, + "only_allow_merge_if_pipeline_succeeds": false, "open_issues_count": 0, "permissions": { "project_access": null, @@ -88,7 +88,7 @@ "avatar_url": null, "star_count": 0, "forks_count": 0, - "only_allow_merge_if_build_succeeds": false, + "only_allow_merge_if_pipeline_succeeds": false, "open_issues_count": 5, "permissions": { "project_access": { @@ -139,7 +139,7 @@ "avatar_url": null, "star_count": 0, "forks_count": 0, - "only_allow_merge_if_build_succeeds": true, + "only_allow_merge_if_pipeline_succeeds": true, "open_issues_count": 4, "permissions": { "project_access": null, @@ -187,7 +187,7 @@ "avatar_url": null, "star_count": 0, "forks_count": 0, - "only_allow_merge_if_build_succeeds": true, + "only_allow_merge_if_pipeline_succeeds": true, "open_issues_count": 4, "permissions": { "project_access": null, @@ -235,7 +235,7 @@ "avatar_url": null, "star_count": 0, "forks_count": 0, - "only_allow_merge_if_build_succeeds": false, + "only_allow_merge_if_pipeline_succeeds": false, "open_issues_count": 5, "permissions": { "project_access": null, @@ -283,7 +283,7 @@ "avatar_url": null, "star_count": 0, "forks_count": 0, - "only_allow_merge_if_build_succeeds": false, + "only_allow_merge_if_pipeline_succeeds": false, "open_issues_count": 5, "permissions": { "project_access": { @@ -334,7 +334,7 @@ "avatar_url": null, "star_count": 0, "forks_count": 0, - "only_allow_merge_if_build_succeeds": false, + "only_allow_merge_if_pipeline_succeeds": false, "open_issues_count": 3, "permissions": { "project_access": null, @@ -382,7 +382,7 @@ "avatar_url": null, "star_count": 0, "forks_count": 0, - "only_allow_merge_if_build_succeeds": false, + "only_allow_merge_if_pipeline_succeeds": false, "open_issues_count": 5, "permissions": { "project_access": { @@ -433,7 +433,7 @@ "avatar_url": null, "star_count": 0, "forks_count": 0, - "only_allow_merge_if_build_succeeds": false, + "only_allow_merge_if_pipeline_succeeds": false, "open_issues_count": 5, "permissions": { "project_access": null, diff --git a/spec/javascripts/lib/utils/text_utility_spec.js.es6 b/spec/javascripts/lib/utils/text_utility_spec.js.es6 index 06b69b8ac17..4200e943121 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js.es6 +++ b/spec/javascripts/lib/utils/text_utility_spec.js.es6 @@ -46,5 +46,65 @@ require('~/lib/utils/text_utility'); expect(gl.text.highCountTrim(45)).toBe(45); }); }); + + describe('gl.text.insertText', () => { + let textArea; + + beforeAll(() => { + textArea = document.createElement('textarea'); + document.querySelector('body').appendChild(textArea); + }); + + afterAll(() => { + textArea.parentNode.removeChild(textArea); + }); + + describe('without selection', () => { + it('inserts the tag on an empty line', () => { + const initialValue = ''; + + textArea.value = initialValue; + textArea.selectionStart = 0; + textArea.selectionEnd = 0; + + gl.text.insertText(textArea, textArea.value, '*', null, '', false); + + expect(textArea.value).toEqual(`${initialValue}* `); + }); + + it('inserts the tag on a new line if the current one is not empty', () => { + const initialValue = 'some text'; + + textArea.value = initialValue; + textArea.setSelectionRange(initialValue.length, initialValue.length); + + gl.text.insertText(textArea, textArea.value, '*', null, '', false); + + expect(textArea.value).toEqual(`${initialValue}\n* `); + }); + + it('inserts the tag on the same line if the current line only contains spaces', () => { + const initialValue = ' '; + + textArea.value = initialValue; + textArea.setSelectionRange(initialValue.length, initialValue.length); + + gl.text.insertText(textArea, textArea.value, '*', null, '', false); + + expect(textArea.value).toEqual(`${initialValue}* `); + }); + + it('inserts the tag on the same line if the current line only contains tabs', () => { + const initialValue = '\t\t\t'; + + textArea.value = initialValue; + textArea.setSelectionRange(initialValue.length, initialValue.length); + + gl.text.insertText(textArea, textArea.value, '*', null, '', false); + + expect(textArea.value).toEqual(`${initialValue}* `); + }); + }); + }); }); })(); diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js index f132537b943..90a429beeca 100644 --- a/spec/javascripts/new_branch_spec.js +++ b/spec/javascripts/new_branch_spec.js @@ -1,7 +1,6 @@ /* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, max-len */ /* global NewBranchForm */ -require('jquery-ui/ui/autocomplete'); require('~/new_branch_form'); (function() { diff --git a/spec/javascripts/user_callout_spec.js.es6 b/spec/javascripts/user_callout_spec.js.es6 index 6ee63f56a26..205e72af600 100644 --- a/spec/javascripts/user_callout_spec.js.es6 +++ b/spec/javascripts/user_callout_spec.js.es6 @@ -3,35 +3,55 @@ const UserCallout = require('~/user_callout'); const USER_CALLOUT_COOKIE = 'user_callout_dismissed'; const Cookie = window.Cookies; -describe('UserCallout', () => { +describe('UserCallout', function () { const fixtureName = 'static/user_callout.html.raw'; preloadFixtures(fixtureName); - beforeEach(function () { + beforeEach(() => { loadFixtures(fixtureName); + Cookie.remove(USER_CALLOUT_COOKIE); + this.userCallout = new UserCallout(); this.closeButton = $('.close-user-callout'); this.userCalloutBtn = $('.user-callout-btn'); this.userCalloutContainer = $('.user-callout'); - Cookie.set(USER_CALLOUT_COOKIE, 'false'); }); - afterEach(function () { - Cookie.set(USER_CALLOUT_COOKIE, 'false'); + it('does not show when cookie is set not defined', () => { + expect(Cookie.get(USER_CALLOUT_COOKIE)).toBeUndefined(); + expect(this.userCalloutContainer.is(':visible')).toBe(true); }); - it('shows when cookie is set to false', function () { + it('shows when cookie is set to false', () => { + Cookie.set(USER_CALLOUT_COOKIE, 'false'); + expect(Cookie.get(USER_CALLOUT_COOKIE)).toBeDefined(); expect(this.userCalloutContainer.is(':visible')).toBe(true); }); - it('hides when user clicks on the dismiss-icon', function () { + it('hides when user clicks on the dismiss-icon', () => { this.closeButton.click(); expect(Cookie.get(USER_CALLOUT_COOKIE)).toBe('true'); }); - it('hides when user clicks on the "check it out" button', function () { + it('hides when user clicks on the "check it out" button', () => { this.userCalloutBtn.click(); expect(Cookie.get(USER_CALLOUT_COOKIE)).toBe('true'); }); }); + +describe('UserCallout when cookie is present', function () { + const fixtureName = 'static/user_callout.html.raw'; + preloadFixtures(fixtureName); + + beforeEach(() => { + loadFixtures(fixtureName); + Cookie.set(USER_CALLOUT_COOKIE, 'true'); + this.userCallout = new UserCallout(); + this.userCalloutContainer = $('.user-callout'); + }); + + it('removes the DOM element', () => { + expect(this.userCalloutContainer.length).toBe(0); + }); +}); diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 b/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 index dd495cb43bc..9cb067921a7 100644 --- a/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 +++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 @@ -6,12 +6,10 @@ describe('Pagination component', () => { const changeChanges = { one: '', - two: '', }; - const change = (one, two) => { + const change = (one) => { changeChanges.one = one; - changeChanges.two = two; }; it('should render and start at page 1', () => { @@ -34,7 +32,6 @@ describe('Pagination component', () => { component.changePage({ target: { innerText: '1' } }); expect(changeChanges.one).toEqual(1); - expect(changeChanges.two).toEqual(null); }); it('should go to the previous page', () => { @@ -55,7 +52,6 @@ describe('Pagination component', () => { component.changePage({ target: { innerText: 'Prev' } }); expect(changeChanges.one).toEqual(1); - expect(changeChanges.two).toEqual(null); }); it('should go to the next page', () => { @@ -76,7 +72,6 @@ describe('Pagination component', () => { component.changePage({ target: { innerText: 'Next' } }); expect(changeChanges.one).toEqual(5); - expect(changeChanges.two).toEqual(null); }); it('should go to the last page', () => { @@ -97,7 +92,6 @@ describe('Pagination component', () => { component.changePage({ target: { innerText: 'Last >>' } }); expect(changeChanges.one).toEqual(10); - expect(changeChanges.two).toEqual(null); }); it('should go to the first page', () => { @@ -118,7 +112,6 @@ describe('Pagination component', () => { component.changePage({ target: { innerText: '<< First' } }); expect(changeChanges.one).toEqual(1); - expect(changeChanges.two).toEqual(null); }); it('should do nothing', () => { @@ -139,7 +132,6 @@ describe('Pagination component', () => { component.changePage({ target: { innerText: '...' } }); expect(changeChanges.one).toEqual(1); - expect(changeChanges.two).toEqual(null); }); }); diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb index d5d128c1907..9873774909e 100644 --- a/spec/lib/banzai/filter/user_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb @@ -123,6 +123,12 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do expect(doc.css('a').first.attr('href')).to eq urls.group_url(group) end + + it 'has the full group name as a title' do + doc = reference_filter("Hey #{reference}") + + expect(doc.css('a').first.attr('title')).to eq group.full_name + end end it 'links with adjacent text' do diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 1919542ca25..3f11f0a4516 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -47,7 +47,7 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe :branch_names do + describe '#branch_names' do subject { repository.branch_names } it 'has SeedRepo::Repo::BRANCHES.size elements' do @@ -57,7 +57,7 @@ describe Gitlab::Git::Repository, seed_helper: true do it { is_expected.not_to include("branch-from-space") } end - describe :tag_names do + describe '#tag_names' do subject { repository.tag_names } it { is_expected.to be_kind_of Array } @@ -78,49 +78,63 @@ describe Gitlab::Git::Repository, seed_helper: true do it { expect(metadata['ArchivePath']).to end_with extenstion } end - describe :archive do + describe '#archive_prefix' do + let(:project_name) { 'project-name'} + + before do + expect(repository).to receive(:name).once.and_return(project_name) + end + + it 'returns parameterised string for a ref containing slashes' do + prefix = repository.archive_prefix('test/branch', 'SHA') + + expect(prefix).to eq("#{project_name}-test-branch-SHA") + end + end + + describe '#archive' do let(:metadata) { repository.archive_metadata('master', '/tmp') } it_should_behave_like 'archive check', '.tar.gz' end - describe :archive_zip do + describe '#archive_zip' do let(:metadata) { repository.archive_metadata('master', '/tmp', 'zip') } it_should_behave_like 'archive check', '.zip' end - describe :archive_bz2 do + describe '#archive_bz2' do let(:metadata) { repository.archive_metadata('master', '/tmp', 'tbz2') } it_should_behave_like 'archive check', '.tar.bz2' end - describe :archive_fallback do + describe '#archive_fallback' do let(:metadata) { repository.archive_metadata('master', '/tmp', 'madeup') } it_should_behave_like 'archive check', '.tar.gz' end - describe :size do + describe '#size' do subject { repository.size } it { is_expected.to be < 2 } end - describe :has_commits? do + describe '#has_commits?' do it { expect(repository.has_commits?).to be_truthy } end - describe :empty? do + describe '#empty?' do it { expect(repository.empty?).to be_falsey } end - describe :bare? do + describe '#bare?' do it { expect(repository.bare?).to be_truthy } end - describe :heads do + describe '#heads' do let(:heads) { repository.heads } subject { heads } @@ -147,7 +161,7 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe :ref_names do + describe '#ref_names' do let(:ref_names) { repository.ref_names } subject { ref_names } @@ -164,7 +178,7 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe :search_files do + describe '#search_files' do let(:results) { repository.search_files('rails', 'master') } subject { results } @@ -200,7 +214,7 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - context :submodules do + context '#submodules' do let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } context 'where repo has submodules' do @@ -264,7 +278,7 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe :commit_count do + describe '#commit_count' do it { expect(repository.commit_count("master")).to eq(25) } it { expect(repository.commit_count("feature")).to eq(9) } end diff --git a/spec/lib/gitlab/gitaly_client/notifications_spec.rb b/spec/lib/gitlab/gitaly_client/notifications_spec.rb new file mode 100644 index 00000000000..a6252c99aa1 --- /dev/null +++ b/spec/lib/gitlab/gitaly_client/notifications_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe Gitlab::GitalyClient::Notifications do + let(:client) { Gitlab::GitalyClient::Notifications.new } + + before do + allow(Gitlab.config.gitaly).to receive(:socket_path).and_return('path/to/gitaly.socket') + end + + describe '#post_receive' do + let(:repo_path) { '/path/to/my_repo.git' } + + it 'sends a post_receive message' do + expect_any_instance_of(Gitaly::Notifications::Stub). + to receive(:post_receive).with(post_receive_request_with_repo_path(repo_path)) + + client.post_receive(repo_path) + end + end +end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 06617f3b007..eef283c2460 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -153,6 +153,7 @@ project: - gitlab_issue_tracker_service - external_wiki_service - kubernetes_service +- mock_ci_service - forked_project_link - forked_from_project - forked_project_links diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 2e9f60432b4..c3d5c451a3c 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -2539,7 +2539,7 @@ "merge_params": { "force_remove_source_branch": null }, - "merge_when_build_succeeds": true, + "merge_when_pipeline_succeeds": true, "merge_user_id": null, "merge_commit_sha": null, "deleted_at": null, @@ -2976,7 +2976,7 @@ "merge_params": { "force_remove_source_branch": null }, - "merge_when_build_succeeds": false, + "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, "deleted_at": null, @@ -3260,7 +3260,7 @@ "merge_params": { "force_remove_source_branch": null }, - "merge_when_build_succeeds": false, + "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, "deleted_at": null, @@ -3544,7 +3544,7 @@ "merge_params": { "force_remove_source_branch": null }, - "merge_when_build_succeeds": false, + "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, "deleted_at": null, @@ -4234,7 +4234,7 @@ "merge_params": { "force_remove_source_branch": null }, - "merge_when_build_succeeds": false, + "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, "deleted_at": null, @@ -4782,7 +4782,7 @@ "merge_params": { "force_remove_source_branch": null }, - "merge_when_build_succeeds": false, + "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, "deleted_at": null, @@ -5281,7 +5281,7 @@ "merge_params": { "force_remove_source_branch": null }, - "merge_when_build_succeeds": false, + "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, "deleted_at": null, @@ -5541,7 +5541,7 @@ "merge_params": { "force_remove_source_branch": null }, - "merge_when_build_succeeds": false, + "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, "deleted_at": null, @@ -6231,7 +6231,7 @@ "merge_params": { "force_remove_source_branch": null }, - "merge_when_build_succeeds": false, + "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, "deleted_at": null, diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index c5ac702d831..6534902b52d 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -142,7 +142,7 @@ MergeRequest: - updated_by_id - merge_error - merge_params -- merge_when_build_succeeds +- merge_when_pipeline_succeeds - merge_user_id - merge_commit_sha - deleted_at diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index b963ca4e542..5743c555cbe 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -181,20 +181,6 @@ describe Ci::Build, :models do end end - describe '#create_from' do - before do - build.status = 'success' - build.save - end - let(:create_from_build) { Ci::Build.create_from build } - - it 'exists a pending task' do - expect(Ci::Build.pending.count(:all)).to eq 0 - create_from_build - expect(Ci::Build.pending.count(:all)).to be > 0 - end - end - describe '#depends_on_builds' do let!(:build) { create(:ci_build, pipeline: pipeline, name: 'build', stage_idx: 0, stage: 'build') } let!(:rspec_test) { create(:ci_build, pipeline: pipeline, name: 'rspec', stage_idx: 1, stage: 'test') } diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index fa1b0396bcf..e000d0d38b3 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -37,12 +37,12 @@ describe MergeRequest, models: true do end it "is invalid without merge user" do - subject.merge_when_build_succeeds = true + subject.merge_when_pipeline_succeeds = true expect(subject).not_to be_valid end it "is valid with merge user" do - subject.merge_when_build_succeeds = true + subject.merge_when_pipeline_succeeds = true subject.merge_user = build(:user) expect(subject).to be_valid @@ -55,7 +55,7 @@ describe MergeRequest, models: true do it { is_expected.to respond_to(:can_be_merged?) } it { is_expected.to respond_to(:cannot_be_merged?) } it { is_expected.to respond_to(:merge_params) } - it { is_expected.to respond_to(:merge_when_build_succeeds) } + it { is_expected.to respond_to(:merge_when_pipeline_succeeds) } end describe '.in_projects' do @@ -508,17 +508,17 @@ describe MergeRequest, models: true do end end - describe "#reset_merge_when_build_succeeds" do + describe "#reset_merge_when_pipeline_succeeds" do let(:merge_if_green) do - create :merge_request, merge_when_build_succeeds: true, merge_user: create(:user), + create :merge_request, merge_when_pipeline_succeeds: true, merge_user: create(:user), merge_params: { "should_remove_source_branch" => "1", "commit_message" => "msg" } end it "sets the item to false" do - merge_if_green.reset_merge_when_build_succeeds + merge_if_green.reset_merge_when_pipeline_succeeds merge_if_green.reload - expect(merge_if_green.merge_when_build_succeeds).to be_falsey + expect(merge_if_green.merge_when_pipeline_succeeds).to be_falsey expect(merge_if_green.merge_params["should_remove_source_branch"]).to be_nil expect(merge_if_green.merge_params["commit_message"]).to be_nil end @@ -812,7 +812,7 @@ describe MergeRequest, models: true do end describe '#check_if_can_be_merged' do - let(:project) { create(:empty_project, only_allow_merge_if_build_succeeds: true) } + let(:project) { create(:empty_project, only_allow_merge_if_pipeline_succeeds: true) } subject { create(:merge_request, source_project: project, merge_status: :unchecked) } @@ -833,12 +833,6 @@ describe MergeRequest, models: true do it 'becomes unmergeable' do expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged') end - - it 'creates Todo on unmergeability' do - expect_any_instance_of(TodoService).to receive(:merge_request_became_unmergeable).with(subject) - - subject.check_if_can_be_merged - end end context 'when it has conflicts' do @@ -933,7 +927,7 @@ describe MergeRequest, models: true do end describe '#mergeable_ci_state?' do - let(:project) { create(:empty_project, only_allow_merge_if_build_succeeds: true) } + let(:project) { create(:empty_project, only_allow_merge_if_pipeline_succeeds: true) } let(:pipeline) { create(:ci_empty_pipeline) } subject { build(:merge_request, target_project: project) } @@ -976,7 +970,7 @@ describe MergeRequest, models: true do end context 'when merges are not restricted to green builds' do - subject { build(:merge_request, target_project: build(:empty_project, only_allow_merge_if_build_succeeds: false)) } + subject { build(:merge_request, target_project: build(:empty_project, only_allow_merge_if_pipeline_succeeds: false)) } context 'and a failed pipeline is associated' do before do @@ -1581,7 +1575,7 @@ describe MergeRequest, models: true do status: status) end - let(:project) { create(:project, :public, :repository, only_allow_merge_if_build_succeeds: true) } + let(:project) { create(:project, :public, :repository, only_allow_merge_if_pipeline_succeeds: true) } let(:developer) { create(:user) } let(:user) { create(:user) } let(:merge_request) { create(:merge_request, source_project: project) } diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb index 59a4ae1b799..9b711bfc007 100644 --- a/spec/models/project_group_link_spec.rb +++ b/spec/models/project_group_link_spec.rb @@ -7,12 +7,27 @@ describe ProjectGroupLink do end describe "Validation" do - let!(:project_group_link) { create(:project_group_link) } + let(:parent_group) { create(:group) } + let(:group) { create(:group, parent: parent_group) } + let(:project) { create(:project, group: group) } + let!(:project_group_link) { create(:project_group_link, project: project) } it { should validate_presence_of(:project_id) } it { should validate_uniqueness_of(:group_id).scoped_to(:project_id).with_message(/already shared/) } it { should validate_presence_of(:group) } it { should validate_presence_of(:group_access) } + + it "doesn't allow a project to be shared with the group it is in" do + project_group_link.group = group + + expect(project_group_link).not_to be_valid + end + + it "doesn't allow a project to be shared with an ancestor of the group it is in" do + project_group_link.group = parent_group + + expect(project_group_link).not_to be_valid + end end describe "destroying a record", truncate: true do diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 24356447fed..585c899cdf9 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -163,6 +163,12 @@ describe KubernetesService, models: true, caching: true do { key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true } ) end + + it 'sets KUBE_CA_PEM_FILE' do + expect(subject.predefined_variables).to include( + { key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true } + ) + end end describe '#terminals' do diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index ae203fada12..eb992e1354e 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1112,16 +1112,16 @@ describe Repository, models: true do let(:update_image_commit) { repository.commit('2f63565e7aac07bcdadb654e253078b727143ec4') } context 'when there is a conflict' do - it 'aborts the operation' do - expect(repository.revert(user, new_image_commit, 'master')).to eq(false) + it 'raises an error' do + expect { repository.revert(user, new_image_commit, 'master') }.to raise_error(/Failed to/) end end context 'when commit was already reverted' do - it 'aborts the operation' do + it 'raises an error' do repository.revert(user, update_image_commit, 'master') - expect(repository.revert(user, update_image_commit, 'master')).to eq(false) + expect { repository.revert(user, update_image_commit, 'master') }.to raise_error(/Failed to/) end end @@ -1148,16 +1148,16 @@ describe Repository, models: true do let(:pickable_merge) { repository.commit('e56497bb5f03a90a51293fc6d516788730953899') } context 'when there is a conflict' do - it 'aborts the operation' do - expect(repository.cherry_pick(user, conflict_commit, 'master')).to eq(false) + it 'raises an error' do + expect { repository.cherry_pick(user, conflict_commit, 'master') }.to raise_error(/Failed to/) end end context 'when commit was already cherry-picked' do - it 'aborts the operation' do + it 'raises an error' do repository.cherry_pick(user, pickable_commit, 'master') - expect(repository.cherry_pick(user, pickable_commit, 'master')).to eq(false) + expect { repository.cherry_pick(user, pickable_commit, 'master') }.to raise_error(/Failed to/) end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 6356f8b6c92..e86b4a761d9 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1429,7 +1429,7 @@ describe User, models: true do it { expect(user.nested_groups).to eq([nested_group]) } end - describe '#nested_projects' do + describe '#nested_groups_projects' do let!(:user) { create(:user) } let!(:group) { create(:group) } let!(:nested_group) { create(:group, parent: group) } @@ -1444,7 +1444,7 @@ describe User, models: true do other_project.add_developer(create(:user)) end - it { expect(user.nested_projects).to eq([nested_project]) } + it { expect(user.nested_groups_projects).to eq([nested_project]) } end describe '#refresh_authorized_projects', redis: true do diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb index 919c98d6437..46edbd49b28 100644 --- a/spec/requests/api/access_requests_spec.rb +++ b/spec/requests/api/access_requests_spec.rb @@ -200,7 +200,7 @@ describe API::AccessRequests, api: true do expect do delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", access_requester) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) end.to change { source.requesters.count }.by(-1) end end @@ -210,7 +210,7 @@ describe API::AccessRequests, api: true do expect do delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", master) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) end.to change { source.requesters.count }.by(-1) end diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb index 6cc1ef315db..9756991162e 100644 --- a/spec/requests/api/award_emoji_spec.rb +++ b/spec/requests/api/award_emoji_spec.rb @@ -242,9 +242,9 @@ describe API::AwardEmoji, api: true do it 'deletes the award' do expect do delete api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user) - end.to change { issue.award_emoji.count }.from(1).to(0) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) + 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 @@ -258,9 +258,9 @@ describe API::AwardEmoji, api: true do it 'deletes the award' do expect do delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user) - end.to change { merge_request.award_emoji.count }.from(1).to(0) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) + end.to change { merge_request.award_emoji.count }.from(1).to(0) end it 'returns a 404 error when note id not found' do @@ -277,9 +277,9 @@ describe API::AwardEmoji, api: true do it 'deletes the award' do expect do delete api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user) - end.to change { snippet.award_emoji.count }.from(1).to(0) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) + end.to change { snippet.award_emoji.count }.from(1).to(0) end end end @@ -290,9 +290,9 @@ describe API::AwardEmoji, api: true do it 'deletes the award' do expect do delete api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user) - end.to change { note.award_emoji.count }.from(1).to(0) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) + end.to change { note.award_emoji.count }.from(1).to(0) end end end diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb index 71df534ebe1..87c36639cd4 100644 --- a/spec/requests/api/boards_spec.rb +++ b/spec/requests/api/boards_spec.rb @@ -195,8 +195,7 @@ describe API::Boards, api: true do it "deletes the list if an admin requests it" do delete api("#{base_url}/#{dev_list.id}", owner) - expect(response).to have_http_status(200) - expect(json_response['position']).to eq(1) + expect(response).to have_http_status(204) end end end diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index cacdb21c692..ab5a7e4d3de 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -325,15 +325,14 @@ describe API::Branches, api: true do it "removes branch" do delete api("/projects/#{project.id}/repository/branches/#{branch_name}", user) - expect(response).to have_http_status(200) - expect(json_response['branch']).to eq(branch_name) + + expect(response).to have_http_status(204) end it "removes a branch with dots in the branch name" do delete api("/projects/#{project.id}/repository/branches/with.1.2.3", user) - expect(response).to have_http_status(200) - expect(json_response['branch']).to eq("with.1.2.3") + expect(response).to have_http_status(204) end it 'returns 404 if branch not exists' do diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb index 921d8714173..024fa66848c 100644 --- a/spec/requests/api/broadcast_messages_spec.rb +++ b/spec/requests/api/broadcast_messages_spec.rb @@ -174,8 +174,11 @@ describe API::BroadcastMessages, api: true do end it 'deletes the broadcast message for admins' do - expect { delete api("/broadcast_messages/#{message.id}", admin) } - .to change { BroadcastMessage.count }.by(-1) + expect do + delete api("/broadcast_messages/#{message.id}", admin) + + expect(response).to have_http_status(204) + end.to change { BroadcastMessage.count }.by(-1) end end end diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb index 81a8856b8f1..d8b3cc041a5 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -151,26 +151,62 @@ describe API::CommitStatuses, api: true do end context 'with all optional parameters' do - before do - optional_params = { state: 'success', - context: 'coverage', - ref: 'develop', - description: 'test', - coverage: 80.0, - target_url: 'http://gitlab.com/status' } - - post api(post_url, developer), optional_params + context 'when creating a commit status' do + it 'creates commit status' do + post api(post_url, developer), { + state: 'success', + context: 'coverage', + ref: 'develop', + description: 'test', + coverage: 80.0, + target_url: 'http://gitlab.com/status' + } + + expect(response).to have_http_status(201) + expect(json_response['sha']).to eq(commit.id) + expect(json_response['status']).to eq('success') + expect(json_response['name']).to eq('coverage') + expect(json_response['ref']).to eq('develop') + expect(json_response['coverage']).to eq(80.0) + expect(json_response['description']).to eq('test') + expect(json_response['target_url']).to eq('http://gitlab.com/status') + end end - it 'creates commit status' do - expect(response).to have_http_status(201) - expect(json_response['sha']).to eq(commit.id) - expect(json_response['status']).to eq('success') - expect(json_response['name']).to eq('coverage') - expect(json_response['ref']).to eq('develop') - expect(json_response['coverage']).to eq(80.0) - expect(json_response['description']).to eq('test') - expect(json_response['target_url']).to eq('http://gitlab.com/status') + context 'when updatig a commit status' do + before do + post api(post_url, developer), { + state: 'running', + context: 'coverage', + ref: 'develop', + description: 'coverage test', + coverage: 0.0, + target_url: 'http://gitlab.com/status' + } + + post api(post_url, developer), { + state: 'success', + name: 'coverage', + ref: 'develop', + description: 'new description', + coverage: 90.0 + } + end + + it 'updates a commit status' do + expect(response).to have_http_status(201) + expect(json_response['sha']).to eq(commit.id) + expect(json_response['status']).to eq('success') + expect(json_response['name']).to eq('coverage') + expect(json_response['ref']).to eq('develop') + expect(json_response['coverage']).to eq(90.0) + expect(json_response['description']).to eq('new description') + expect(json_response['target_url']).to eq('http://gitlab.com/status') + end + + it 'does not create a new commit status' do + expect(CommitStatus.count).to eq 1 + end end end diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 7e682e91bd1..4f4b18cf0e0 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -116,6 +116,8 @@ describe API::DeployKeys, api: true do it 'should delete existing key' do expect do delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) + + expect(response).to have_http_status(204) end.to change{ project.deploy_keys.count }.by(-1) end diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index d0958d39d44..f2fd1dfc8db 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -24,6 +24,7 @@ describe API::Environments, api: true do 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']).to be_present end end @@ -122,7 +123,7 @@ describe API::Environments, api: true do it 'returns a 200 for an existing environment' do delete api("/projects/#{project.id}/environments/#{environment.id}", user) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) end it 'returns a 404 for non existing id' do @@ -141,4 +142,39 @@ describe API::Environments, api: true do end end end + + describe 'POST /projects/:id/environments/:environment_id/stop' do + context 'as a master' do + context 'with a stoppable environment' do + before do + environment.update(state: :available) + + post api("/projects/#{project.id}/environments/#{environment.id}/stop", user) + end + + it 'returns a 200' do + expect(response).to have_http_status(200) + end + + it 'actually stops the environment' do + expect(environment.reload).to be_stopped + end + end + + it 'returns a 404 for non existing id' do + post api("/projects/#{project.id}/environments/12345/stop", user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Not found') + end + end + + context 'a non member' do + it 'rejects the request' do + post api("/projects/#{project.id}/environments/#{environment.id}/stop", non_member) + + expect(response).to have_http_status(404) + end + end + end end diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index 29d67b5259e..91f8a35e045 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -147,6 +147,20 @@ describe API::Files, api: true do 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 api("/projects/#{project.id}/repository/files", user), valid_params + + expect(response).to have_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 @@ -201,11 +215,7 @@ describe API::Files, api: true do it "deletes existing file in project repo" do delete api("/projects/#{project.id}/repository/files", user), valid_params - expect(response).to have_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) + expect(response).to have_http_status(204) end it "returns a 400 bad request if no params given" do @@ -228,10 +238,7 @@ describe API::Files, api: true do delete api("/projects/#{project.id}/repository/files", user), valid_params - expect(response).to have_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) + expect(response).to have_http_status(204) end end end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index fb3dc1b074e..2b8fd7e31a1 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -176,7 +176,7 @@ describe API::Groups, api: true do 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['visibility']).to eq(Gitlab::VisibilityLevel.string_level(group1.visibility_level)) expect(json_response['avatar_url']).to eq(group1.avatar_url) expect(json_response['web_url']).to eq(group1.web_url) expect(json_response['request_access_enabled']).to eq(group1.request_access_enabled) @@ -295,7 +295,7 @@ describe API::Groups, api: true do 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 + expect(json_response.first['visibility']).to be_present end it "returns the group's projects with simple representation" do @@ -306,7 +306,7 @@ describe API::Groups, api: true do 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 + expect(json_response.first['visibility']).not_to be_present end it 'filters the groups projects' do @@ -467,7 +467,7 @@ describe API::Groups, api: true do it "removes group" do delete api("/groups/#{group1.id}", user1) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) end it "does not remove a group if not an owner" do @@ -496,7 +496,7 @@ describe API::Groups, api: true do it "removes any existing group" do delete api("/groups/#{group2.id}", admin) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) end it "does not remove a non existing group" do diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index ffeacb15f17..f18b8e98707 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -409,6 +409,34 @@ describe API::Internal, api: true do end end + describe 'POST /notify_post_receive' do + let(:valid_params) do + { repo_path: project.repository.path, secret_token: secret_token } + end + + before do + allow(Gitlab.config.gitaly).to receive(:socket_path).and_return('path/to/gitaly.socket') + end + + it "calls the Gitaly client if it's enabled" do + expect_any_instance_of(Gitlab::GitalyClient::Notifications). + to receive(:post_receive).with(project.repository.path) + + post api("/internal/notify_post_receive"), valid_params + + expect(response).to have_http_status(200) + end + + it "returns 500 if the gitaly call fails" do + expect_any_instance_of(Gitlab::GitalyClient::Notifications). + to receive(:post_receive).with(project.repository.path).and_raise(GRPC::Unavailable) + + post api("/internal/notify_post_receive"), valid_params + + expect(response).to have_http_status(500) + end + end + def project_with_repo_path(path) double().tap do |fake_project| allow(fake_project).to receive_message_chain('repository.path_to_repo' => path) diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 7cb75310204..710e4320fd1 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -212,6 +212,25 @@ describe API::Issues, api: true do expect(json_response.first['id']).to eq(confidential_issue.id) end + it 'returns an array of issues found by iids' do + get api('/issues', user), iids: [closed_issue.iid] + + expect(response).to have_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['id']).to eq(closed_issue.id) + end + + it 'returns an empty array if iid does not exist' do + get api("/issues", user), iids: [99999] + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + it 'sorts by created_at descending by default' do get api('/issues', user) @@ -377,6 +396,25 @@ describe API::Issues, api: true do expect(json_response.first['labels']).to eq([label_c.title, label_b.title, group_label.title]) end + it 'returns an array of issues found by iids' do + get api(base_url, user), iids: [group_issue.iid] + + expect(response).to have_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['id']).to eq(group_issue.id) + end + + it 'returns an empty array if iid does not exist' do + get api(base_url, user), iids: [99999] + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + 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 api("#{base_url}?labels=foo,bar", user) @@ -586,6 +624,25 @@ describe API::Issues, api: true do expect(json_response.first['labels']).to eq([label_c.title, label_b.title, label.title]) end + it 'returns an array of issues found by iids' do + get api("#{base_url}/issues", user), iids: [issue.iid] + + expect(response).to have_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['id']).to eq(issue.id) + end + + it 'returns an empty array if iid does not exist' do + get api("#{base_url}/issues", user), iids: [99999] + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + it 'returns an empty array if not all labels matches' do get api("#{base_url}/issues?labels=#{label.title},foo", user) @@ -1175,8 +1232,8 @@ describe API::Issues, api: true do it "deletes the issue if an admin requests it" do delete api("/projects/#{project.id}/issues/#{issue.id}", owner) - expect(response).to have_http_status(200) - expect(json_response['state']).to eq 'opened' + + expect(response).to have_http_status(204) end end diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index af271dbd4f5..a1adaba7b98 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -175,9 +175,10 @@ describe API::Labels, api: true do end describe 'DELETE /projects/:id/labels' do - it 'returns 200 for existing label' do + it 'returns 204 for existing label' do delete api("/projects/#{project.id}/labels", user), name: 'label1' - expect(response).to have_http_status(200) + + expect(response).to have_http_status(204) end it 'returns 404 for non existing label' do diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index 127498ed109..2d37d026a39 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -263,18 +263,18 @@ describe API::Members, api: true do expect do delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", developer) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) 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 + it 'returns 404' do expect do delete api("/#{source_type.pluralize}/#{source.id}/members/#{access_requester.id}", master) - expect(response).to have_http_status(source_type == 'project' ? 200 : 404) + expect(response).to have_http_status(404) end.not_to change { source.requesters.count } end end @@ -283,15 +283,15 @@ describe API::Members, api: true do expect do delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) end.to change { source.members.count }.by(-1) end end - it "returns #{source_type == 'project' ? 200 : 404} if member does not exist" do + it 'returns 404 if member does not exist' do delete api("/#{source_type.pluralize}/#{source.id}/members/123", master) - expect(response).to have_http_status(source_type == 'project' ? 200 : 404) + expect(response).to have_http_status(404) end end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index b87d0cd7de9..b3f0876c822 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -170,7 +170,7 @@ describe API::MergeRequests, api: true do 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_when_pipeline_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 @@ -411,7 +411,7 @@ describe API::MergeRequests, api: true do it "destroys the merge request owners can destroy" do delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) end end end @@ -483,11 +483,11 @@ describe API::MergeRequests, api: true do allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(pipeline) allow(pipeline).to receive(:active?).and_return(true) - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_build_succeeds: true + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_pipeline_succeeds: true expect(response).to have_http_status(200) expect(json_response['title']).to eq('Test') - expect(json_response['merge_when_build_succeeds']).to eq(true) + expect(json_response['merge_when_pipeline_succeeds']).to eq(true) end end diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb index 418bf5a507c..78c230117b8 100644 --- a/spec/requests/api/milestones_spec.rb +++ b/spec/requests/api/milestones_spec.rb @@ -4,8 +4,8 @@ describe API::Milestones, api: true do include ApiHelpers let(:user) { create(:user) } let!(:project) { create(:empty_project, namespace: user.namespace ) } - let!(:closed_milestone) { create(:closed_milestone, project: project) } - let!(:milestone) { create(:milestone, project: project) } + let!(:closed_milestone) { create(:closed_milestone, project: project, title: 'version1', description: 'closed milestone') } + let!(:milestone) { create(:milestone, project: project, title: 'version2', description: 'open milestone') } before { project.team << [user, :developer] } @@ -45,8 +45,37 @@ describe API::Milestones, api: true do expect(json_response.first['id']).to eq(closed_milestone.id) end - it 'returns a project milestone by iid' do - get api("/projects/#{project.id}/milestones?iid=#{closed_milestone.iid}", user) + it 'returns an array of milestones specified by iids' do + other_milestone = create(:milestone, project: project) + + get api("/projects/#{project.id}/milestones", user), iids: [closed_milestone.iid, other_milestone.iid] + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.map{ |m| m['id'] }).to match_array([closed_milestone.id, other_milestone.id]) + end + + it 'does not return any milestone if none found' do + get api("/projects/#{project.id}/milestones", user), iids: [Milestone.maximum(:iid).succ] + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + end + + describe 'GET /projects/:id/milestones/:milestone_id' do + it 'returns a project milestone by id' do + get api("/projects/#{project.id}/milestones/#{milestone.id}", user) + + expect(response).to have_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 iids array' do + get api("/projects/#{project.id}/milestones?iids=#{closed_milestone.iid}", user) expect(response.status).to eq 200 expect(response).to include_pagination_headers @@ -56,21 +85,22 @@ describe API::Milestones, api: true do expect(json_response.first['id']).to eq closed_milestone.id end - it 'returns a project milestone by iid array' do - get api("/projects/#{project.id}/milestones", user), iid: [milestone.iid, closed_milestone.iid] + it 'returns a project milestone by searching for title' do + get api("/projects/#{project.id}/milestones", user), search: 'version2' expect(response).to have_http_status(200) - expect(json_response.size).to eq(2) + expect(response).to include_pagination_headers + expect(json_response.size).to eq(1) expect(json_response.first['title']).to eq milestone.title expect(json_response.first['id']).to eq milestone.id end - it 'returns a project milestone by iid array' do - get api("/projects/#{project.id}/milestones", user), iid: [milestone.iid, closed_milestone.iid] + it 'returns a project milestones by searching for description' do + get api("/projects/#{project.id}/milestones", user), search: 'open' expect(response).to have_http_status(200) expect(response).to include_pagination_headers - expect(json_response.size).to eq(2) + expect(json_response.size).to eq(1) expect(json_response.first['title']).to eq milestone.title expect(json_response.first['id']).to eq milestone.id end diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 3cca4468be7..347f8f6fa3b 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -225,11 +225,11 @@ describe API::Notes, api: true do context 'when the user is posting an award emoji on an issue created by someone else' do let(:issue2) { create(:issue, project: project) } - it 'returns an award emoji' do + it 'creates a new issue note' do post api("/projects/#{project.id}/issues/#{issue2.id}/notes", user), body: ':+1:' expect(response).to have_http_status(201) - expect(json_response['awardable_id']).to eq issue2.id + expect(json_response['body']).to eq(':+1:') end end @@ -373,7 +373,7 @@ describe API::Notes, api: true do delete api("/projects/#{project.id}/issues/#{issue.id}/"\ "notes/#{issue_note.id}", user) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) # Check if note is really deleted delete api("/projects/#{project.id}/issues/#{issue.id}/"\ "notes/#{issue_note.id}", user) @@ -392,7 +392,7 @@ describe API::Notes, api: true do delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\ "notes/#{snippet_note.id}", user) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) # Check if note is really deleted delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\ "notes/#{snippet_note.id}", user) @@ -412,7 +412,7 @@ describe API::Notes, api: true do delete api("/projects/#{project.id}/merge_requests/"\ "#{merge_request.id}/notes/#{merge_request_note.id}", user) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) # Check if note is really deleted delete api("/projects/#{project.id}/merge_requests/"\ "#{merge_request.id}/notes/#{merge_request_note.id}", user) diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index 20c76bd2c05..f286568547d 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -183,13 +183,9 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do it "deletes hook from project" do expect do delete api("/projects/#{project.id}/hooks/#{hook.id}", user) - end.to change {project.hooks.count}.by(-1) - expect(response).to have_http_status(200) - end - it "returns success when deleting hook" do - delete api("/projects/#{project.id}/hooks/#{hook.id}", user) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) + end.to change {project.hooks.count}.by(-1) end it "returns a 404 error when deleting non existent hook" do diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index da9df56401b..9e88c19b0bc 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -44,7 +44,7 @@ describe API::ProjectSnippets, api: true do title: 'Test Title', file_name: 'test.rb', code: 'puts "hello world"', - visibility_level: Snippet::PUBLIC + visibility: 'public' } end @@ -56,7 +56,7 @@ describe API::ProjectSnippets, api: true do 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]) + expect(snippet.visibility_level).to eq(Snippet::PUBLIC) end it 'returns 400 for missing parameters' do @@ -80,14 +80,14 @@ describe API::ProjectSnippets, api: true do context 'when the snippet is private' do it 'creates the snippet' do - expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }. + expect { create_snippet(project, visibility: '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) }. + it 'rejects the snippet' do + expect { create_snippet(project, visibility: 'public') }. not_to change { Snippet.count } expect(response).to have_http_status(400) @@ -95,7 +95,7 @@ describe API::ProjectSnippets, api: true do end it 'creates a spam log' do - expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. + expect { create_snippet(project, visibility: 'public') }. to change { SpamLog.count }.by(1) end end @@ -165,7 +165,7 @@ describe API::ProjectSnippets, api: true do let(:visibility_level) { Snippet::PRIVATE } it 'rejects the snippet' do - expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }. + expect { update_snippet(title: 'Foo', visibility: 'public') }. not_to change { snippet.reload.title } expect(response).to have_http_status(400) @@ -173,7 +173,7 @@ describe API::ProjectSnippets, api: true do end it 'creates a spam log' do - expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }. + expect { update_snippet(title: 'Foo', visibility: 'public') }. to change { SpamLog.count }.by(1) end end @@ -189,7 +189,7 @@ describe API::ProjectSnippets, api: true do delete api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) end it 'returns 404 for invalid snippet id' do @@ -212,7 +212,7 @@ describe API::ProjectSnippets, api: true do end it 'returns 404 for invalid snippet id' do - delete api("/projects/#{snippet.project.id}/snippets/1234", admin) + get api("/projects/#{snippet.project.id}/snippets/1234/raw", admin) expect(response).to have_http_status(404) expect(json_response['message']).to eq('404 Snippet Not Found') diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 5de4426f3bd..03cae074803 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -43,9 +43,10 @@ describe API::Projects, api: true do describe 'GET /projects' do shared_examples_for 'projects response' do it 'returns an array of projects' do - get api('/projects', current_user) + get api('/projects', current_user), filter expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.map { |p| p['id'] }).to contain_exactly(*projects.map(&:id)) end @@ -61,6 +62,7 @@ describe API::Projects, api: true do context 'when unauthenticated' do it_behaves_like 'projects response' do + let(:filter) { {} } let(:current_user) { nil } let(:projects) { [public_project] } end @@ -68,6 +70,7 @@ describe API::Projects, api: true do context 'when authenticated as regular user' do it_behaves_like 'projects response' do + let(:filter) { {} } let(:current_user) { user } let(:projects) { [public_project, project, project2, project3] } end @@ -133,13 +136,18 @@ describe API::Projects, api: true do end context 'and using search' do - it 'returns searched project' do - get api('/projects', user), { search: project.name } + it_behaves_like 'projects response' do + let(:filter) { { search: project.name } } + let(:current_user) { user } + let(:projects) { [project] } + end + end - expect(response).to have_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) + context 'and membership=true' do + it_behaves_like 'projects response' do + let(:filter) { { membership: true } } + let(:current_user) { user } + let(:projects) { [project, project2, project3] } end end @@ -216,36 +224,52 @@ describe API::Projects, api: true do end context 'and with all query parameters' do - # | | project5 | project6 | project7 | project8 | project9 | - # |---------+----------+----------+----------+----------+----------| - # | search | x | | x | x | x | - # | starred | x | x | | x | x | - # | public | x | x | x | | x | - # | owned | x | x | x | x | | - let!(:project5) { create(:empty_project, :public, path: 'gitlab5', namespace: user.namespace) } + let!(:project5) { create(:empty_project, :public, path: 'gitlab5', namespace: create(:namespace)) } let!(:project6) { create(:empty_project, :public, path: 'project6', namespace: user.namespace) } let!(:project7) { create(:empty_project, :public, path: 'gitlab7', namespace: user.namespace) } let!(:project8) { create(:empty_project, path: 'gitlab8', namespace: user.namespace) } let!(:project9) { create(:empty_project, :public, path: 'gitlab9') } before do - user.update_attributes(starred_projects: [project5, project6, project8, project9]) + user.update_attributes(starred_projects: [project5, project7, project8, project9]) end - it 'returns only projects that satify all query parameters' do - get api('/projects', user), { visibility: 'public', owned: true, starred: true, search: 'gitlab' } + context 'including owned filter' do + it 'returns only projects that satisfy all query parameters' do + get api('/projects', user), { visibility: 'public', owned: true, starred: true, search: 'gitlab' } - expect(response).to have_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.size).to eq(1) - expect(json_response.first['id']).to eq(project5.id) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response.first['id']).to eq(project7.id) + end + end + + context 'including membership filter' do + before do + create(:project_member, + user: user, + project: project5, + access_level: ProjectMember::MASTER) + end + + it 'returns only projects that satisfy all query parameters' do + get api('/projects', user), { visibility: 'public', membership: true, starred: true, search: 'gitlab' } + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(2) + expect(json_response.map { |project| project['id'] }).to contain_exactly(project5.id, project7.id) + end end end end context 'when authenticated as a different user' do it_behaves_like 'projects response' do + let(:filter) { {} } let(:current_user) { user2 } let(:projects) { [public_project] } end @@ -253,6 +277,7 @@ describe API::Projects, api: true do context 'when authenticated as admin' do it_behaves_like 'projects response' do + let(:filter) { {} } let(:current_user) { admin } let(:projects) { Project.all } end @@ -269,10 +294,37 @@ describe API::Projects, api: true do end end - it 'creates new project without path and return 201' do - expect { post api('/projects', user), name: 'foo' }. + it 'creates new project without path but with name and returns 201' do + expect { post api('/projects', user), name: 'Foo Project' }. to change { Project.count }.by(1) expect(response).to have_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 api('/projects', user), path: 'foo_project' }. + to change { Project.count }.by(1) + expect(response).to have_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 api('/projects', user), path: 'foo-Project', name: 'Foo Project' }. + to change { Project.count }.by(1) + expect(response).to have_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 @@ -281,7 +333,7 @@ describe API::Projects, api: true do expect(response).to have_http_status(201) end - it 'does not create new project without name and return 400' do + it 'does not create new project without name or path and returns 400' do expect { post api('/projects', user) }.not_to change { Project.count } expect(response).to have_http_status(400) end @@ -293,7 +345,7 @@ describe API::Projects, api: true do issues_enabled: false, merge_requests_enabled: false, wiki_enabled: false, - only_allow_merge_if_build_succeeds: false, + only_allow_merge_if_pipeline_succeeds: false, request_access_enabled: true, only_allow_merge_if_all_discussions_are_resolved: false }) @@ -313,36 +365,39 @@ describe API::Projects, api: true do end it 'sets a project as public' do - project = attributes_for(:project, :public) + project = attributes_for(:project, visibility: 'public') + post api('/projects', user), project - expect(json_response['public']).to be_truthy - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) + + expect(json_response['visibility']).to eq('public') end it 'sets a project as internal' do - project = attributes_for(:project, :internal) + project = attributes_for(:project, visibility: 'internal') + post api('/projects', user), project - expect(json_response['public']).to be_falsey - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) + + expect(json_response['visibility']).to eq('internal') end it 'sets a project as private' do - project = attributes_for(:project, :private) + project = attributes_for(:project, visibility: 'private') + post api('/projects', user), project - expect(json_response['public']).to be_falsey - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) + + expect(json_response['visibility']).to eq('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 }) + project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: false }) post api('/projects', user), project - expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey + expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_falsey end - it 'sets a project as allowing merge only if build succeeds' do - project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true }) + it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do + project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: true }) post api('/projects', user), project - expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy + expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_truthy end it 'sets a project as allowing merge even if discussions are unresolved' do @@ -370,7 +425,7 @@ describe API::Projects, api: true do end context 'when a visibility level is restricted' do - let(:project_param) { attributes_for(:project, :public) } + let(:project_param) { attributes_for(:project, visibility: 'public') } before do stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) @@ -388,10 +443,7 @@ describe API::Projects, api: true do it 'allows an admin to override restricted visibility settings' do post api('/projects', admin), project_param - expect(json_response['public']).to be_truthy - expect(json_response['visibility_level']).to( - eq(Gitlab::VisibilityLevel::PUBLIC) - ) + expect(json_response['visibility']).to eq('public') end end end @@ -432,40 +484,41 @@ describe API::Projects, api: true do end it 'sets a project as public' do - project = attributes_for(:project, :public) + project = attributes_for(:project, visibility: 'public') + post api("/projects/user/#{user.id}", admin), project expect(response).to have_http_status(201) - expect(json_response['public']).to be_truthy - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) + expect(json_response['visibility']).to eq('public') end it 'sets a project as internal' do - project = attributes_for(:project, :internal) + project = attributes_for(:project, visibility: 'internal') + post api("/projects/user/#{user.id}", admin), project expect(response).to have_http_status(201) - expect(json_response['public']).to be_falsey - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) + expect(json_response['visibility']).to eq('internal') end it 'sets a project as private' do - project = attributes_for(:project, :private) + project = attributes_for(:project, visibility: 'private') + post api("/projects/user/#{user.id}", admin), project - expect(json_response['public']).to be_falsey - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) + + expect(json_response['visibility']).to eq('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 }) + project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: false }) post api("/projects/user/#{user.id}", admin), project - expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey + expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_falsey end - it 'sets a project as allowing merge only if build succeeds' do - project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true }) + it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do + project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: true }) post api("/projects/user/#{user.id}", admin), project - expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy + expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_truthy end it 'sets a project as allowing merge even if discussions are unresolved' do @@ -529,9 +582,8 @@ describe API::Projects, api: true do 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['visibility']).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 @@ -559,7 +611,7 @@ describe API::Projects, api: true do 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_build_succeeds) + expect(json_response['only_allow_merge_if_pipeline_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 @@ -785,8 +837,7 @@ describe API::Projects, api: true do describe 'POST /projects/:id/snippets' do it 'creates a new project snippet' do post api("/projects/#{project.id}/snippets", user), - title: 'api test', file_name: 'sample.rb', code: 'test', - visibility_level: Gitlab::VisibilityLevel::PRIVATE + title: 'api test', file_name: 'sample.rb', code: 'test', visibility: 'private' expect(response).to have_http_status(201) expect(json_response['title']).to eq('api test') end @@ -820,8 +871,9 @@ describe API::Projects, api: true do it 'deletes existing project snippet' do expect do delete api("/projects/#{project.id}/snippets/#{snippet.id}", user) + + expect(response).to have_http_status(204) end.to change { Snippet.count }.by(-1) - expect(response).to have_http_status(200) end it 'returns 404 when deleting unknown snippet id' do @@ -905,8 +957,10 @@ describe API::Projects, api: true do project_fork_target.reload expect(project_fork_target.forked_from_project).not_to be_nil expect(project_fork_target.forked?).to be_truthy + delete api("/projects/#{project_fork_target.id}/fork", admin) - expect(response).to have_http_status(200) + + expect(response).to have_http_status(204) project_fork_target.reload expect(project_fork_target.forked_from_project).to be_nil expect(project_fork_target.forked?).not_to be_truthy @@ -1035,7 +1089,7 @@ describe API::Projects, api: true do end it 'updates visibility_level' do - project_param = { visibility_level: Gitlab::VisibilityLevel::PUBLIC } + project_param = { visibility: 'public' } put api("/projects/#{project3.id}", user), project_param expect(response).to have_http_status(200) project_param.each_pair do |k, v| @@ -1045,13 +1099,13 @@ describe API::Projects, api: true do it 'updates visibility_level from public to private' do project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC }) - project_param = { visibility_level: Gitlab::VisibilityLevel::PRIVATE } + project_param = { visibility: 'private' } put api("/projects/#{project3.id}", user), project_param expect(response).to have_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) + expect(json_response['visibility']).to eq('private') end it 'does not update name to existing name' do @@ -1118,7 +1172,7 @@ describe API::Projects, api: true do end it 'does not update visibility_level' do - project_param = { visibility_level: Gitlab::VisibilityLevel::PUBLIC } + project_param = { visibility: 'public' } put api("/projects/#{project3.id}", user4), project_param expect(response).to have_http_status(403) end diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index 73e82647ca0..e83202e4196 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -123,6 +123,7 @@ describe API::Runner do context 'when no token is provided' do it 'returns 400 error' do delete api('/runners') + expect(response).to have_http_status 400 end end @@ -130,6 +131,7 @@ describe API::Runner do context 'when invalid token is provided' do it 'returns 403 error' do delete api('/runners'), token: 'invalid' + expect(response).to have_http_status 403 end end @@ -139,7 +141,8 @@ describe API::Runner do it 'deletes Runner' do delete api('/runners'), token: runner.token - expect(response).to have_http_status 200 + + expect(response).to have_http_status 204 expect(Ci::Runner.count).to eq(0) end end diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb index 103d6755888..8a82543a830 100644 --- a/spec/requests/api/runners_spec.rb +++ b/spec/requests/api/runners_spec.rb @@ -277,8 +277,9 @@ describe API::Runners, api: true do it 'deletes runner' do expect do delete api("/runners/#{shared_runner.id}", admin) + + expect(response).to have_http_status(204) end.to change{ Ci::Runner.shared.count }.by(-1) - expect(response).to have_http_status(200) end end @@ -286,15 +287,17 @@ describe API::Runners, api: true do it 'deletes unused runner' do expect do delete api("/runners/#{unused_specific_runner.id}", admin) + + expect(response).to have_http_status(204) end.to change{ Ci::Runner.specific.count }.by(-1) - expect(response).to have_http_status(200) end it 'deletes used runner' do expect do delete api("/runners/#{specific_runner.id}", admin) + + expect(response).to have_http_status(204) end.to change{ Ci::Runner.specific.count }.by(-1) - expect(response).to have_http_status(200) end end @@ -327,8 +330,9 @@ describe API::Runners, api: true do it 'deletes runner for one owned project' do expect do delete api("/runners/#{specific_runner.id}", user) + + expect(response).to have_http_status(204) end.to change{ Ci::Runner.specific.count }.by(-1) - expect(response).to have_http_status(200) end end end @@ -457,8 +461,9 @@ describe API::Runners, api: true do it "disables project's runner" do expect do delete api("/projects/#{project.id}/runners/#{two_projects_runner.id}", user) + + expect(response).to have_http_status(204) end.to change{ project.runners.count }.by(-1) - expect(response).to have_http_status(200) end end diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index 776dc655650..fd334934ca5 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -55,7 +55,7 @@ describe API::Services, api: true do it "deletes #{service}" do delete api("/projects/#{project.id}/services/#{dashed_service}", user) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) project.send(service_method).reload expect(project.send(service_method).activated?).to be_falsey end diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 411905edb49..11b4b718e2c 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -18,6 +18,9 @@ describe API::Settings, 'Settings', api: true do expect(json_response['koding_url']).to be_nil expect(json_response['plantuml_enabled']).to be_falsey expect(json_response['plantuml_url']).to be_nil + expect(json_response['default_project_visibility']).to be_a String + expect(json_response['default_snippet_visibility']).to be_a String + expect(json_response['default_group_visibility']).to be_a String end end @@ -37,6 +40,8 @@ describe API::Settings, 'Settings', api: true do koding_url: 'http://koding.example.com', plantuml_enabled: true, plantuml_url: 'http://plantuml.example.com', + default_snippet_visibility: 'internal', + restricted_visibility_levels: ['public'], default_artifacts_expire_in: '2 days' expect(response).to have_http_status(200) expect(json_response['default_projects_limit']).to eq(3) @@ -47,6 +52,8 @@ describe API::Settings, 'Settings', api: true do 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') + expect(json_response['default_snippet_visibility']).to eq('internal') + expect(json_response['restricted_visibility_levels']).to eq(['public']) expect(json_response['default_artifacts_expire_in']).to eq('2 days') end end diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index 41def7cd1d4..5d75b47b3cd 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -74,7 +74,7 @@ describe API::Snippets, api: true do end it 'returns 404 for invalid snippet id' do - delete api("/snippets/1234", user) + get api("/snippets/1234/raw", user) expect(response).to have_http_status(404) expect(json_response['message']).to eq('404 Snippet Not Found') @@ -87,7 +87,7 @@ describe API::Snippets, api: true do title: 'Test Title', file_name: 'test.rb', content: 'puts "hello world"', - visibility_level: Snippet::PUBLIC + visibility: 'public' } end @@ -120,14 +120,14 @@ describe API::Snippets, api: true do context 'when the snippet is private' do it 'creates the snippet' do - expect { create_snippet(visibility_level: Snippet::PRIVATE) }. + expect { create_snippet(visibility: '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) }. + expect { create_snippet(visibility: 'public') }. not_to change { Snippet.count } expect(response).to have_http_status(400) @@ -135,7 +135,7 @@ describe API::Snippets, api: true do end it 'creates a spam log' do - expect { create_snippet(visibility_level: Snippet::PUBLIC) }. + expect { create_snippet(visibility: 'public') }. to change { SpamLog.count }.by(1) end end @@ -218,12 +218,12 @@ describe API::Snippets, api: true do let(:visibility_level) { Snippet::PRIVATE } it 'rejects the snippet' do - expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }. + expect { update_snippet(title: 'Foo', visibility: 'public') }. not_to change { snippet.reload.title } end it 'creates a spam log' do - expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }. + expect { update_snippet(title: 'Foo', visibility: 'public') }. to change { SpamLog.count }.by(1) end end diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb index b59da632c00..d1e10f12657 100644 --- a/spec/requests/api/system_hooks_spec.rb +++ b/spec/requests/api/system_hooks_spec.rb @@ -91,6 +91,8 @@ describe API::SystemHooks, api: true do it "deletes a hook" do expect do delete api("/hooks/#{hook.id}", admin) + + expect(response).to have_http_status(204) end.to change { SystemHook.count }.by(-1) end diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb index 8a4f078182f..b132d033a61 100644 --- a/spec/requests/api/tags_spec.rb +++ b/spec/requests/api/tags_spec.rb @@ -137,8 +137,8 @@ describe API::Tags, api: true do context 'delete tag' do it 'deletes an existing tag' do delete api("/projects/#{project.id}/repository/tags/#{tag_name}", user) - expect(response).to have_http_status(200) - expect(json_response['tag_name']).to eq(tag_name) + + expect(response).to have_http_status(204) end it 'raises 404 if the tag does not exist' do diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb index f35e963a14b..1e401935662 100644 --- a/spec/requests/api/todos_spec.rb +++ b/spec/requests/api/todos_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe API::Todos, api: true do include ApiHelpers - let(:project_1) { create(:empty_project) } + let(:project_1) { create(:empty_project, :test_repo) } let(:project_2) { create(:empty_project) } let(:author_1) { create(:user) } let(:author_2) { create(:user) } @@ -11,7 +11,7 @@ describe API::Todos, api: true do let(:merge_request) { create(:merge_request, source_project: project_1) } 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!(:pending_3) { create(:on_commit_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 diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index 92dfc2aa277..153e2791cbe 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -190,8 +190,9 @@ describe API::Triggers do it 'deletes trigger' do expect do delete api("/projects/#{project.id}/triggers/#{trigger.token}", user) + + expect(response).to have_http_status(204) end.to change{project.triggers.count}.by(-1) - expect(response).to have_http_status(200) end it 'responds with 404 Not Found if requesting non-existing trigger' do diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 603da9f49fc..e5e4c84755f 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -540,10 +540,12 @@ describe API::Users, api: true do it 'deletes existing key' do user.keys << key user.save + expect do delete api("/users/#{user.id}/keys/#{key.id}", admin) + + expect(response).to have_http_status(204) end.to change { user.keys.count }.by(-1) - expect(response).to have_http_status(200) end it 'returns 404 error if user not found' do @@ -637,10 +639,12 @@ describe API::Users, api: true do it 'deletes existing email' do user.emails << email user.save + expect do delete api("/users/#{user.id}/emails/#{email.id}", admin) + + expect(response).to have_http_status(204) end.to change { user.emails.count }.by(-1) - expect(response).to have_http_status(200) end it 'returns 404 error if user not found' do @@ -671,10 +675,10 @@ describe API::Users, api: true do it "deletes user" do delete api("/users/#{user.id}", admin) - expect(response).to have_http_status(200) + + expect(response).to have_http_status(204) expect { User.find(user.id) }.to raise_error ActiveRecord::RecordNotFound expect { Namespace.find(namespace.id) }.to raise_error ActiveRecord::RecordNotFound - expect(json_response['email']).to eq(user.email) end it "does not delete for unauthenticated user" do @@ -869,10 +873,12 @@ describe API::Users, api: true do it "deletes existed key" do user.keys << key user.save + expect do delete api("/user/keys/#{key.id}", user) + + expect(response).to have_http_status(204) end.to change{user.keys.count}.by(-1) - expect(response).to have_http_status(200) end it "returns 404 if key ID not found" do @@ -976,10 +982,12 @@ describe API::Users, api: true do it "deletes existed email" do user.emails << email user.save + expect do delete api("/user/emails/#{email.id}", user) + + expect(response).to have_http_status(204) end.to change{user.emails.count}.by(-1) - expect(response).to have_http_status(200) end it "returns 404 if email ID not found" do diff --git a/spec/requests/api/v3/award_emoji_spec.rb b/spec/requests/api/v3/award_emoji_spec.rb new file mode 100644 index 00000000000..91145c8e72c --- /dev/null +++ b/spec/requests/api/v3/award_emoji_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' + +describe API::V3::AwardEmoji, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let!(:project) { create(:empty_project) } + let(:issue) { create(:issue, project: project) } + let!(: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) } + let!(:note) { create(:note, project: project, noteable: issue) } + + before { project.team << [user, :master] } + + 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_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_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_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_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_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_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 index 8aaf3be4f87..eb95934f354 100644 --- a/spec/requests/api/v3/boards_spec.rb +++ b/spec/requests/api/v3/boards_spec.rb @@ -5,6 +5,7 @@ describe API::V3::Boards, api: true do let(:user) { create(:user) } let(:guest) { create(:user) } + let(:non_member) { create(:user) } let!(:project) { create(:empty_project, :public, creator_id: user.id, namespace: user.namespace ) } let!(:dev_label) do @@ -76,4 +77,37 @@ describe API::V3::Boards, api: true do expect(response).to have_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_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_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_http_status(404) + end + + context "when the user is project owner" do + let(:owner) { create(:user) } + let(:project) { create(:empty_project, namespace: owner.namespace) } + + it "deletes the list if an admin requests it" do + delete v3_api("#{base_url}/#{dev_list.id}", owner) + + expect(response).to have_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 index a3e1581fcc5..e4cedf98e64 100644 --- a/spec/requests/api/v3/branches_spec.rb +++ b/spec/requests/api/v3/branches_spec.rb @@ -5,8 +5,12 @@ describe API::V3::Branches, api: true do include ApiHelpers 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) } + let!(:guest) { create(:project_member, :guest, user: user2, project: project) } + let!(:branch_name) { 'feature' } + 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 @@ -21,6 +25,44 @@ describe API::V3::Branches, api: true do 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_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_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_http_status(404) + end + + it "removes protected branch" do + create(:protected_branch, project: project, name: branch_name) + delete v3_api("/projects/#{project.id}/repository/branches/#{branch_name}", user) + expect(response).to have_http_status(405) + expect(json_response['message']).to eq('Protected branch cant be removed') + end + + it "does not remove HEAD branch" do + delete v3_api("/projects/#{project.id}/repository/branches/master", user) + expect(response).to have_http_status(405) + expect(json_response['message']).to eq('Cannot remove HEAD branch') + end + end + describe "DELETE /projects/:id/repository/merged_branches" do before do allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true) @@ -33,10 +75,7 @@ describe API::V3::Branches, api: true do end it 'returns a 403 error if guest' do - user_b = create :user - create(:project_member, :guest, user: user_b, project: project) - - delete v3_api("/projects/#{project.id}/repository/merged_branches", user_b) + delete v3_api("/projects/#{project.id}/repository/merged_branches", user2) expect(response).to have_http_status(403) end diff --git a/spec/requests/api/v3/broadcast_messages_spec.rb b/spec/requests/api/v3/broadcast_messages_spec.rb new file mode 100644 index 00000000000..06556401a29 --- /dev/null +++ b/spec/requests/api/v3/broadcast_messages_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe API::V3::BroadcastMessages, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:admin) { create(:admin) } + + describe 'DELETE /broadcast_messages/:id' do + let!(: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_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_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_http_status(200) + end.to change { BroadcastMessage.count }.by(-1) + end + end +end diff --git a/spec/requests/api/v3/environments_spec.rb b/spec/requests/api/v3/environments_spec.rb new file mode 100644 index 00000000000..216192c9d34 --- /dev/null +++ b/spec/requests/api/v3/environments_spec.rb @@ -0,0 +1,165 @@ +require 'spec_helper' + +describe API::V3::Environments, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:non_member) { create(:user) } + let(:project) { create(:empty_project, :private, namespace: user.namespace) } + let!(:environment) { create(:environment, project: project) } + + before do + project.team << [user, :master] + 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_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_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_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_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_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_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_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_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_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_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_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_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_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_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 index 52fd908af7d..3b61139a2cd 100644 --- a/spec/requests/api/v3/files_spec.rb +++ b/spec/requests/api/v3/files_spec.rb @@ -2,17 +2,6 @@ require 'spec_helper' describe API::V3::Files, api: true do include ApiHelpers - 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) { FFaker::Internet.email } # I have to remove periods from the end of the name # This happened when the user's name had a suffix (i.e. "Sr.") @@ -26,6 +15,18 @@ describe API::V3::Files, api: true do # ... # 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) { FFaker::Internet.email } let(:author_name) { FFaker::Name.name.chomp("\.") } before { project.team << [user, :developer] } @@ -147,6 +148,20 @@ describe API::V3::Files, api: true do 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_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 diff --git a/spec/requests/api/v3/groups_spec.rb b/spec/requests/api/v3/groups_spec.rb index 8b29ad03737..a71b7d4b008 100644 --- a/spec/requests/api/v3/groups_spec.rb +++ b/spec/requests/api/v3/groups_spec.rb @@ -4,14 +4,144 @@ describe API::V3::Groups, api: true do include ApiHelpers 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(:empty_project, namespace: group1) } let!(:project2) { create(:empty_project, namespace: group2) } + let!(:project3) { create(:empty_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_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_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_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_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_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_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_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_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_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_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_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 @@ -32,4 +162,404 @@ describe API::V3::Groups, api: true do end end end + + describe "GET /groups/:id" do + context "when authenticated as user" do + it "returns one of user1's groups" do + project = create(:empty_project, namespace: group2, path: 'Foo') + create(:project_group_link, project: project, group: group1) + + get v3_api("/groups/#{group1.id}", user1) + + expect(response).to have_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) + 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_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_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_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_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_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_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_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_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_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_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_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_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_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_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(:empty_project, :public, path: 'test1', group: group1) + + get v3_api("/groups/#{group1.id}/projects", user1), visibility: 'public' + + expect(response).to have_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_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_http_status(404) + end + + it "only returns projects to which user has access" do + project3.team << [user3, :developer] + + get v3_api("/groups/#{group1.id}/projects", user3) + + expect(response).to have_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_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_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_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_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_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_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_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_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_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" 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_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_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_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_http_status(400) + end + end + end + + describe "DELETE /groups/:id" do + context "when authenticated as user" do + it "removes group" do + delete v3_api("/groups/#{group1.id}", user1) + + expect(response).to have_http_status(200) + 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_http_status(403) + end + + it "does not remove a non existing group" do + delete v3_api("/groups/1328", user1) + + expect(response).to have_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_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_http_status(200) + end + + it "does not remove a non existing group" do + delete v3_api("/groups/1328", admin) + + expect(response).to have_http_status(404) + end + end + end + + describe "POST /groups/:id/projects/:project_id" do + let(:project) { create(:empty_project) } + let(:project_path) { "#{project.namespace.path}%2F#{project.path}" } + + before(:each) 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_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_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_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_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_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_http_status(404) + end + end + end + end + end end diff --git a/spec/requests/api/v3/labels_spec.rb b/spec/requests/api/v3/labels_spec.rb index f44403374e9..dfac357d37c 100644 --- a/spec/requests/api/v3/labels_spec.rb +++ b/spec/requests/api/v3/labels_spec.rb @@ -149,4 +149,23 @@ describe API::V3::Labels, api: true do 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_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_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_http_status(400) + end + end end diff --git a/spec/requests/api/v3/members_spec.rb b/spec/requests/api/v3/members_spec.rb index 28c3ca03960..13814ed10c3 100644 --- a/spec/requests/api/v3/members_spec.rb +++ b/spec/requests/api/v3/members_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::Members, api: true do +describe API::V3::Members, api: true do include ApiHelpers let(:master) { create(:user) } diff --git a/spec/requests/api/v3/milestones_spec.rb b/spec/requests/api/v3/milestones_spec.rb new file mode 100644 index 00000000000..77705d8c839 --- /dev/null +++ b/spec/requests/api/v3/milestones_spec.rb @@ -0,0 +1,232 @@ +require 'spec_helper' + +describe API::V3::Milestones, api: true do + include ApiHelpers + let(:user) { create(:user) } + let!(:project) { create(:empty_project, namespace: user.namespace ) } + let!(:closed_milestone) { create(:closed_milestone, project: project) } + let!(:milestone) { create(:milestone, project: project) } + + before { project.team << [user, :developer] } + + describe 'GET /projects/:id/milestones' do + it 'returns project milestones' do + get v3_api("/projects/#{project.id}/milestones", user) + + expect(response).to have_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['milestone']['title']).to eq(milestone.title) + 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_http_status(401) + end + + describe 'confidential issues' do + let(:public_project) { create(:empty_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.team << [user, :developer] + 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_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.team << [member, :guest] + + get v3_api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", member) + + expect(response).to have_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_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 index b51cb3055d5..ddef2d5eb04 100644 --- a/spec/requests/api/v3/notes_spec.rb +++ b/spec/requests/api/v3/notes_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe API::V3::Notes, api: true do include ApiHelpers + let(:user) { create(:user) } let!(:project) { create(:empty_project, :public, namespace: user.namespace) } let!(:issue) { create(:issue, project: project, author: user) } @@ -227,11 +228,11 @@ describe API::V3::Notes, api: true do context 'when the user is posting an award emoji on an issue created by someone else' do let(:issue2) { create(:issue, project: project) } - it 'returns an award emoji' do + it 'creates a new issue note' do post v3_api("/projects/#{project.id}/issues/#{issue2.id}/notes", user), body: ':+1:' expect(response).to have_http_status(201) - expect(json_response['awardable_id']).to eq issue2.id + expect(json_response['body']).to eq(':+1:') end end @@ -373,12 +374,12 @@ describe API::V3::Notes, api: true 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) + "notes/#{issue_note.id}", user) expect(response).to have_http_status(200) # Check if note is really deleted delete v3_api("/projects/#{project.id}/issues/#{issue.id}/"\ - "notes/#{issue_note.id}", user) + "notes/#{issue_note.id}", user) expect(response).to have_http_status(404) end @@ -392,18 +393,18 @@ describe API::V3::Notes, api: true do 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) + "notes/#{snippet_note.id}", user) expect(response).to have_http_status(200) # Check if note is really deleted delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ - "notes/#{snippet_note.id}", user) + "notes/#{snippet_note.id}", user) expect(response).to have_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) + "notes/12345", user) expect(response).to have_http_status(404) end @@ -412,18 +413,18 @@ describe API::V3::Notes, api: true do 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) + "#{merge_request.id}/notes/#{merge_request_note.id}", user) expect(response).to have_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) + "#{merge_request.id}/notes/#{merge_request_note.id}", user) expect(response).to have_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) + "#{merge_request.id}/notes/12345", user) expect(response).to have_http_status(404) end diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index 662be3f3531..d8bb562587d 100644 --- a/spec/requests/api/v3/projects_spec.rb +++ b/spec/requests/api/v3/projects_spec.rb @@ -309,10 +309,37 @@ describe API::V3::Projects, api: true do end end - it 'creates new project without path and return 201' do - expect { post v3_api('/projects', user), name: 'foo' }. + 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_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_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_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 @@ -321,7 +348,7 @@ describe API::V3::Projects, api: true do expect(response).to have_http_status(201) end - it 'does not create new project without name and return 400' do + 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_http_status(400) end @@ -400,7 +427,7 @@ describe API::V3::Projects, api: true do expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey end - it 'sets a project as allowing merge only if build succeeds' do + 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 @@ -545,7 +572,7 @@ describe API::V3::Projects, api: true do expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey end - it 'sets a project as allowing merge only if build succeeds' do + 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 @@ -642,7 +669,7 @@ describe API::V3::Projects, api: true do 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_build_succeeds) + 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 diff --git a/spec/requests/api/v3/runners_spec.rb b/spec/requests/api/v3/runners_spec.rb new file mode 100644 index 00000000000..ca335ce9cf0 --- /dev/null +++ b/spec/requests/api/v3/runners_spec.rb @@ -0,0 +1,154 @@ +require 'spec_helper' + +describe API::V3::Runners, api: true do + include ApiHelpers + + let(:admin) { create(:user, :admin) } + let(:user) { create(:user) } + let(:user2) { create(:user) } + + let(:project) { create(:empty_project, creator_id: user.id) } + let(:project2) { create(:empty_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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 new file mode 100644 index 00000000000..7e8c8753d02 --- /dev/null +++ b/spec/requests/api/v3/services_spec.rb @@ -0,0 +1,22 @@ +require "spec_helper" + +describe API::V3::Services, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) } + + Service.available_services_names.each do |service| + describe "DELETE /projects/:id/services/#{service.dasherize}" do + include_context service + + it "deletes #{service}" do + delete v3_api("/projects/#{project.id}/services/#{dashed_service}", user) + + expect(response).to have_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 new file mode 100644 index 00000000000..a9fa5adac17 --- /dev/null +++ b/spec/requests/api/v3/settings_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe API::V3::Settings, 'Settings', api: true do + include ApiHelpers + + 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_http_status(200) + expect(json_response).to be_an Hash + expect(json_response['default_projects_limit']).to eq(42) + expect(json_response['signin_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, signin_enabled: 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_http_status(200) + expect(json_response['default_projects_limit']).to eq(3) + expect(json_response['signin_enabled']).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_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_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 new file mode 100644 index 00000000000..05653bd0d51 --- /dev/null +++ b/spec/requests/api/v3/snippets_spec.rb @@ -0,0 +1,187 @@ +require 'rails_helper' + +describe API::V3::Snippets, api: true do + include ApiHelpers + 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_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_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_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_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_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_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_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(:is_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_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_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_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_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_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_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_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 index da58efb6ebf..91038977c82 100644 --- a/spec/requests/api/v3/system_hooks_spec.rb +++ b/spec/requests/api/v3/system_hooks_spec.rb @@ -38,4 +38,20 @@ describe API::V3::SystemHooks, api: true do 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_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_http_status(404) + end + end end diff --git a/spec/requests/api/v3/tags_spec.rb b/spec/requests/api/v3/tags_spec.rb index 6722789d928..6870cfd2668 100644 --- a/spec/requests/api/v3/tags_spec.rb +++ b/spec/requests/api/v3/tags_spec.rb @@ -64,4 +64,26 @@ describe API::V3::Tags, api: true do 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_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_http_status(404) + end + end + end end diff --git a/spec/requests/api/v3/triggers_spec.rb b/spec/requests/api/v3/triggers_spec.rb new file mode 100644 index 00000000000..721ce4a361b --- /dev/null +++ b/spec/requests/api/v3/triggers_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe API::V3::Triggers do + include ApiHelpers + + 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) { create(:ci_trigger, project: project, token: trigger_token) } + + 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_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_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_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_http_status(401) + end + end + end +end diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb index 769f04c5057..0c1413119e0 100644 --- a/spec/requests/api/variables_spec.rb +++ b/spec/requests/api/variables_spec.rb @@ -152,8 +152,9 @@ describe API::Variables, api: true do it 'deletes variable' do expect do delete api("/projects/#{project.id}/variables/#{variable.key}", user) + + expect(response).to have_http_status(204) end.to change{project.variables.count}.by(-1) - expect(response).to have_http_status(200) end it 'responds with 404 Not Found if requesting non-existing variable' do diff --git a/spec/rubocop/cop/custom_error_class_spec.rb b/spec/rubocop/cop/custom_error_class_spec.rb new file mode 100644 index 00000000000..381d7871a40 --- /dev/null +++ b/spec/rubocop/cop/custom_error_class_spec.rb @@ -0,0 +1,111 @@ +require 'spec_helper' + +require 'rubocop' +require 'rubocop/rspec/support' + +require_relative '../../../rubocop/cop/custom_error_class' + +describe RuboCop::Cop::CustomErrorClass do + include CopHelper + + subject(:cop) { described_class.new } + + context 'when a class has a body' do + it 'does nothing' do + inspect_source(cop, 'class CustomError < StandardError; def foo; end; end') + + expect(cop.offenses).to be_empty + end + end + + context 'when a class has no explicit superclass' do + it 'does nothing' do + inspect_source(cop, 'class CustomError; end') + + expect(cop.offenses).to be_empty + end + end + + context 'when a class has a superclass that does not end in Error' do + it 'does nothing' do + inspect_source(cop, 'class CustomError < BasicObject; end') + + expect(cop.offenses).to be_empty + end + end + + context 'when a class is empty and inherits from a class ending in Error' do + context 'when the class is on a single line' do + let(:source) do + <<-SOURCE + module Foo + class CustomError < Bar::Baz::BaseError; end + end + SOURCE + end + + let(:expected) do + <<-EXPECTED + module Foo + CustomError = Class.new(Bar::Baz::BaseError) + end + EXPECTED + end + + it 'registers an offense' do + expected_highlights = source.split("\n")[1].strip + + inspect_source(cop, source) + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([2]) + expect(cop.highlights).to contain_exactly(expected_highlights) + end + end + + it 'autocorrects to the right version' do + autocorrected = autocorrect_source(cop, source, 'foo/custom_error.rb') + + expect(autocorrected).to eq(expected) + end + end + + context 'when the class is on multiple lines' do + let(:source) do + <<-SOURCE + module Foo + class CustomError < Bar::Baz::BaseError + end + end + SOURCE + end + + let(:expected) do + <<-EXPECTED + module Foo + CustomError = Class.new(Bar::Baz::BaseError) + end + EXPECTED + end + + it 'registers an offense' do + expected_highlights = source.split("\n")[1..2].join("\n").strip + + inspect_source(cop, source) + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([2]) + expect(cop.highlights).to contain_exactly(expected_highlights) + end + end + + it 'autocorrects to the right version' do + autocorrected = autocorrect_source(cop, source, 'foo/custom_error.rb') + + expect(autocorrected).to eq(expected) + end + end + end +end diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb deleted file mode 100644 index b3e0a7b9b58..00000000000 --- a/spec/services/ci/image_for_build_service_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -require 'spec_helper' - -module Ci - describe ImageForBuildService, services: true do - let(:service) { ImageForBuildService.new } - let(:project) { FactoryGirl.create(:empty_project) } - let(:commit_sha) { '01234567890123456789' } - let(:pipeline) { project.ensure_pipeline('master', commit_sha) } - let(:build) { FactoryGirl.create(:ci_build, pipeline: pipeline) } - - describe '#execute' do - before { build } - - context 'branch name' do - before { allow(project).to receive(:commit).and_return(OpenStruct.new(sha: commit_sha)) } - before { build.run! } - let(:image) { service.execute(project, ref: 'master') } - - it { expect(image).to be_kind_of(OpenStruct) } - it { expect(image.path.to_s).to include('public/ci/build-running.svg') } - it { expect(image.name).to eq('build-running.svg') } - end - - context 'unknown branch name' do - let(:image) { service.execute(project, ref: 'feature') } - - it { expect(image).to be_kind_of(OpenStruct) } - it { expect(image.path.to_s).to include('public/ci/build-unknown.svg') } - it { expect(image.name).to eq('build-unknown.svg') } - end - - context 'commit sha' do - before { build.run! } - let(:image) { service.execute(project, sha: build.sha) } - - it { expect(image).to be_kind_of(OpenStruct) } - it { expect(image.path.to_s).to include('public/ci/build-running.svg') } - it { expect(image.name).to eq('build-running.svg') } - end - - context 'unknown commit sha' do - let(:image) { service.execute(project, sha: '0000000') } - - it { expect(image).to be_kind_of(OpenStruct) } - it { expect(image.path.to_s).to include('public/ci/build-unknown.svg') } - it { expect(image.name).to eq('build-unknown.svg') } - end - end - end -end diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb index b818dfdd50c..de68fb64726 100644 --- a/spec/services/ci/process_pipeline_service_spec.rb +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -341,7 +341,7 @@ describe Ci::ProcessPipelineService, :services do expect(builds.pending.count).to eq(1) expect(all_builds.count).to eq(4) - # When pending build succeeds in stage test, we enqueue deploy stage. + # When pending merge_when_pipeline_succeeds in stage test, we enqueue deploy stage. # succeed_pending process_pipeline diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb index d9f774a1095..cd7dd53025c 100644 --- a/spec/services/ci/register_build_service_spec.rb +++ b/spec/services/ci/register_build_service_spec.rb @@ -170,6 +170,51 @@ module Ci end end + context 'when first build is stalled' do + before do + pending_build.lock_version = 10 + end + + subject { described_class.new(specific_runner).execute } + + context 'with multiple builds are in queue' do + let!(:other_build) { create :ci_build, pipeline: pipeline } + + before do + allow_any_instance_of(Ci::RegisterBuildService).to receive(:builds_for_specific_runner) + .and_return([pending_build, other_build]) + end + + it "receives second build from the queue" do + expect(subject).to be_valid + expect(subject.build).to eq(other_build) + end + end + + context 'when single build is in queue' do + before do + allow_any_instance_of(Ci::RegisterBuildService).to receive(:builds_for_specific_runner) + .and_return([pending_build]) + end + + it "does not receive any valid result" do + expect(subject).not_to be_valid + end + end + + context 'when there is no build in queue' do + before do + allow_any_instance_of(Ci::RegisterBuildService).to receive(:builds_for_specific_runner) + .and_return([]) + end + + it "does not receive builds but result is valid" do + expect(subject).to be_valid + expect(subject.build).to be_nil + end + end + end + def execute(runner) described_class.new(runner).execute.build end diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index d03f7505eac..65af4e13118 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -10,22 +10,39 @@ describe Ci::RetryBuildService, :services do described_class.new(project, user) end + CLONE_ACCESSORS = described_class::CLONE_ACCESSORS + + REJECT_ACCESSORS = + %i[id status user token coverage trace runner artifacts_expire_at + artifacts_file artifacts_metadata artifacts_size created_at + updated_at started_at finished_at queued_at erased_by + erased_at].freeze + + IGNORE_ACCESSORS = + %i[type lock_version target_url gl_project_id deploy job_id base_tags + commit_id deployments erased_by_id last_deployment project_id + runner_id tag_taggings taggings tags trigger_request_id + user_id].freeze + shared_examples 'build duplication' do let(:build) do - create(:ci_build, :failed, :artifacts_expired, :erased, :trace, - :queued, :coverage, pipeline: pipeline) + create(:ci_build, :failed, :artifacts_expired, :erased, + :queued, :coverage, :tags, :allowed_to_fail, :on_tag, + :teardown_environment, :triggered, :trace, + description: 'some build', pipeline: pipeline) end - describe 'clone attributes' do - described_class::CLONE_ATTRIBUTES.each do |attribute| + describe 'clone accessors' do + CLONE_ACCESSORS.each do |attribute| it "clones #{attribute} build attribute" do + expect(new_build.send(attribute)).to be_present expect(new_build.send(attribute)).to eq build.send(attribute) end end end - describe 'reject attributes' do - described_class::REJECT_ATTRIBUTES.each do |attribute| + describe 'reject acessors' do + REJECT_ACCESSORS.each do |attribute| it "does not clone #{attribute} build attribute" do expect(new_build.send(attribute)).not_to eq build.send(attribute) end @@ -33,12 +50,20 @@ describe Ci::RetryBuildService, :services do end it 'has correct number of known attributes' do - attributes = - described_class::CLONE_ATTRIBUTES + - described_class::IGNORE_ATTRIBUTES + - described_class::REJECT_ATTRIBUTES + known_accessors = CLONE_ACCESSORS + REJECT_ACCESSORS + IGNORE_ACCESSORS + + # :tag_list is a special case, this accessor does not exist + # in reflected associations, comes from `act_as_taggable` and + # we use it to copy tags, instead of reusing tags. + # + current_accessors = + Ci::Build.attribute_names.map(&:to_sym) + + Ci::Build.reflect_on_all_associations.map(&:name) + + [:tag_list] + + current_accessors.uniq! - expect(build.attributes.size).to eq(attributes.size) + expect(known_accessors).to contain_exactly(*current_accessors) end end diff --git a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb index f92978a33a3..c2f205c389d 100644 --- a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb @@ -5,7 +5,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do let(:project) { create(:project) } let(:mr_merge_if_green_enabled) do - create(:merge_request, merge_when_build_succeeds: true, merge_user: user, + create(:merge_request, merge_when_pipeline_succeeds: true, merge_user: user, source_branch: "master", target_branch: 'feature', source_project: project, target_project: project, state: "opened") end @@ -36,7 +36,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do it 'sets the params, merge_user, and flag' do expect(merge_request).to be_valid - expect(merge_request.merge_when_build_succeeds).to be_truthy + expect(merge_request.merge_when_pipeline_succeeds).to be_truthy expect(merge_request.merge_params).to eq commit_message: 'Awesome message' expect(merge_request.merge_user).to be user end @@ -62,7 +62,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do end it 'updates the merge params' do - expect(SystemNoteService).not_to receive(:merge_when_build_succeeds) + expect(SystemNoteService).not_to receive(:merge_when_pipeline_succeeds) service.execute(mr_merge_if_green_enabled) expect(mr_merge_if_green_enabled.merge_params).to have_key(:new_key) @@ -82,7 +82,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do sha: merge_request_head, status: 'success') end - it "merges all merge requests with merge when build succeeds enabled" do + it "merges all merge requests with merge when the pipeline succeeds enabled" do expect(MergeWorker).to receive(:perform_async) service.trigger(triggering_pipeline) end @@ -111,6 +111,31 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do service.trigger(unrelated_pipeline) end end + + context 'when the merge request is not mergeable' do + let(:mr_conflict) do + create(:merge_request, merge_when_pipeline_succeeds: true, merge_user: user, + source_branch: 'master', target_branch: 'feature-conflict', + source_project: project, target_project: project) + end + + let(:conflict_pipeline) do + create(:ci_pipeline, project: project, ref: mr_conflict.source_branch, + sha: mr_conflict.diff_head_sha, status: 'success') + end + + it 'does not merge the merge request' do + expect(MergeWorker).not_to receive(:perform_async) + + service.trigger(conflict_pipeline) + end + + it 'creates todos for unmergeability' do + expect_any_instance_of(TodoService).to receive(:merge_request_became_unmergeable).with(mr_conflict) + + service.trigger(conflict_pipeline) + end + end end describe "#cancel" do @@ -118,8 +143,8 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do service.cancel(mr_merge_if_green_enabled) end - it "resets all the merge_when_build_succeeds params" do - expect(mr_merge_if_green_enabled.merge_when_build_succeeds).to be_falsey + it "resets all the pipeline succeeds params" do + expect(mr_merge_if_green_enabled.merge_when_pipeline_succeeds).to be_falsey expect(mr_merge_if_green_enabled.merge_params).to eq({}) expect(mr_merge_if_green_enabled.merge_user).to be nil end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 983dac6efdb..ff367f54d2a 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -18,7 +18,7 @@ describe MergeRequests::RefreshService, services: true do source_branch: 'master', target_branch: 'feature', target_project: @project, - merge_when_build_succeeds: true, + merge_when_pipeline_succeeds: true, merge_user: @user) @fork_merge_request = create(:merge_request, @@ -62,7 +62,7 @@ describe MergeRequests::RefreshService, services: true do it { expect(@merge_request.notes).not_to be_empty } it { expect(@merge_request).to be_open } - it { expect(@merge_request.merge_when_build_succeeds).to be_falsey } + it { expect(@merge_request.merge_when_pipeline_succeeds).to be_falsey } it { expect(@merge_request.diff_head_sha).to eq(@newrev) } it { expect(@fork_merge_request).to be_open } it { expect(@fork_merge_request.notes).to be_empty } diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index 9c92a5080c6..152c6d20daa 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -102,47 +102,19 @@ describe Notes::CreateService, services: true do expect(subject.note).to eq(params[:note]) end end - end - - describe "award emoji" do - before do - project.team << [user, :master] - end - - it "creates an award emoji" do - opts = { - note: ':smile: ', - noteable_type: 'Issue', - noteable_id: issue.id - } - note = described_class.new(project, user, opts).execute - - expect(note).to be_valid - expect(note.name).to eq('smile') - end - it "creates regular note if emoji name is invalid" do - opts = { - note: ':smile: moretext:', - noteable_type: 'Issue', - noteable_id: issue.id - } - note = described_class.new(project, user, opts).execute - - expect(note).to be_valid - expect(note.note).to eq(opts[:note]) - end - - it "normalizes the emoji name" do - opts = { - note: ':+1:', - noteable_type: 'Issue', - noteable_id: issue.id - } - - expect_any_instance_of(TodoService).to receive(:new_award_emoji).with(issue, user) + describe 'note with emoji only' do + it 'creates regular note' do + opts = { + note: ':smile: ', + noteable_type: 'Issue', + noteable_id: issue.id + } + note = described_class.new(project, user, opts).execute - described_class.new(project, user, opts).execute + expect(note).to be_valid + expect(note.note).to eq(':smile:') + end end end end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 839250b7d84..ebbaea4e59a 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -1050,22 +1050,22 @@ describe NotificationService, services: true do should_not_email(@u_lazy_participant) end - it "notifies the merger when merge_when_build_succeeds is true" do - merge_request.merge_when_build_succeeds = true + it "notifies the merger when the pipeline succeeds is true" do + merge_request.merge_when_pipeline_succeeds = true notification.merge_mr(merge_request, @u_watcher) should_email(@u_watcher) end - it "does not notify the merger when merge_when_build_succeeds is false" do - merge_request.merge_when_build_succeeds = false + it "does not notify the merger when the pipeline succeeds is false" do + merge_request.merge_when_pipeline_succeeds = false notification.merge_mr(merge_request, @u_watcher) should_not_email(@u_watcher) end - it "notifies the merger when merge_when_build_succeeds is false but they've opted into notifications about their activity" do - merge_request.merge_when_build_succeeds = false + it "notifies the merger when the pipeline succeeds is false but they've opted into notifications about their activity" do + merge_request.merge_when_pipeline_succeeds = false @u_watcher.notified_of_own_activity = true notification.merge_mr(merge_request, @u_watcher) diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb index 0b0925983eb..52e8678cb9d 100644 --- a/spec/services/slash_commands/interpret_service_spec.rb +++ b/spec/services/slash_commands/interpret_service_spec.rb @@ -267,6 +267,14 @@ describe SlashCommands::InterpretService, services: true do end end + shared_examples 'award command' do + it 'toggle award 100 emoji if content containts /award :100:' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(emoji_award: "100") + end + end + it_behaves_like 'reopen command' do let(:content) { '/reopen' } let(:issuable) { issue } @@ -654,6 +662,37 @@ describe SlashCommands::InterpretService, services: true do end end + context '/award command' do + it_behaves_like 'award command' do + let(:content) { '/award :100:' } + let(:issuable) { issue } + end + + it_behaves_like 'award command' do + let(:content) { '/award :100:' } + let(:issuable) { merge_request } + end + + context 'ignores command with no argument' do + it_behaves_like 'empty command' do + let(:content) { '/award' } + let(:issuable) { issue } + end + end + + context 'ignores non-existing / invalid emojis' do + it_behaves_like 'empty command' do + let(:content) { '/award noop' } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/award :lorem_ipsum:' } + let(:issuable) { issue } + end + end + end + context '/target_branch command' do let(:non_empty_project) { create(:project) } let(:another_merge_request) { create(:merge_request, author: developer, source_project: non_empty_project) } diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index eca5a418f2a..36a17a3bf2e 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -215,13 +215,13 @@ describe SystemNoteService, services: true do end end - describe '.merge_when_build_succeeds' do + describe '.merge_when_pipeline_succeeds' do let(:pipeline) { build(:ci_pipeline_without_jobs )} let(:noteable) do create(:merge_request, source_project: project, target_project: project) end - subject { described_class.merge_when_build_succeeds(noteable, project, author, noteable.diff_head_commit) } + subject { described_class.merge_when_pipeline_succeeds(noteable, project, author, noteable.diff_head_commit) } it_behaves_like 'a system note' @@ -230,12 +230,12 @@ describe SystemNoteService, services: true do end end - describe '.cancel_merge_when_build_succeeds' do + describe '.cancel_merge_when_pipeline_succeeds' do let(:noteable) do create(:merge_request, source_project: project, target_project: project) end - subject { described_class.cancel_merge_when_build_succeeds(noteable, project, author) } + subject { described_class.cancel_merge_when_pipeline_succeeds(noteable, project, author) } it_behaves_like 'a system note' @@ -418,45 +418,6 @@ describe SystemNoteService, services: true do to be_truthy end end - - context 'when noteable is an Issue' do - let(:issue) { create(:issue, project: project) } - - it 'is truthy when issue is closed' do - issue.close - - expect(described_class.cross_reference_disallowed?(issue, project.commit)). - to be_truthy - end - - it 'is falsey when issue is open' do - expect(described_class.cross_reference_disallowed?(issue, project.commit)). - to be_falsy - end - end - - context 'when noteable is a Merge Request' do - let(:merge_request) { create(:merge_request, :simple, source_project: project) } - - it 'is truthy when merge request is closed' do - allow(merge_request).to receive(:closed?).and_return(:true) - - expect(described_class.cross_reference_disallowed?(merge_request, project.commit)). - to be_truthy - end - - it 'is truthy when merge request is merged' do - allow(merge_request).to receive(:closed?).and_return(:true) - - expect(described_class.cross_reference_disallowed?(merge_request, project.commit)). - to be_truthy - end - - it 'is falsey when merge request is open' do - expect(described_class.cross_reference_disallowed?(merge_request, project.commit)). - to be_falsy - end - end end describe '.cross_reference_exists?' do diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 9f24cc0f3f2..fb9a8462f84 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -680,7 +680,7 @@ describe TodoService, services: true do end it 'creates a pending todo for merge_user' do - mr_unassigned.update(merge_when_build_succeeds: true, merge_user: admin) + mr_unassigned.update(merge_when_pipeline_succeeds: true, merge_user: admin) service.merge_request_build_failed(mr_unassigned) should_create_todo(user: admin, author: admin, target: mr_unassigned, action: Todo::BUILD_FAILED) @@ -700,7 +700,7 @@ describe TodoService, services: true do describe '#merge_request_became_unmergeable' do it 'creates a pending todo for a merge_user' do - mr_unassigned.update(merge_when_build_succeeds: true, merge_user: admin) + mr_unassigned.update(merge_when_pipeline_succeeds: true, merge_user: admin) service.merge_request_became_unmergeable(mr_unassigned) should_create_todo(user: admin, author: admin, target: mr_unassigned, action: Todo::UNMERGEABLE) diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb index 690fe979492..08733d6dcf1 100644 --- a/spec/services/users/refresh_authorized_projects_service_spec.rb +++ b/spec/services/users/refresh_authorized_projects_service_spec.rb @@ -131,6 +131,80 @@ describe Users::RefreshAuthorizedProjectsService do it 'sets the values to the access levels' do expect(hash.values).to eq([Gitlab::Access::MASTER]) end + + context 'personal projects' do + it 'includes the project with the right access level' do + expect(hash[project.id]).to eq(Gitlab::Access::MASTER) + end + end + + context 'projects the user is a member of' do + let!(:other_project) { create(:empty_project) } + + before do + other_project.team.add_reporter(user) + end + + it 'includes the project with the right access level' do + expect(hash[other_project.id]).to eq(Gitlab::Access::REPORTER) + end + end + + context 'projects of groups the user is a member of' do + let(:group) { create(:group) } + let!(:other_project) { create(:project, group: group) } + + before do + group.add_owner(user) + end + + it 'includes the project with the right access level' do + expect(hash[other_project.id]).to eq(Gitlab::Access::OWNER) + end + end + + context 'projects of subgroups of groups the user is a member of' do + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + let!(:other_project) { create(:project, group: nested_group) } + + before do + group.add_master(user) + end + + it 'includes the project with the right access level' do + expect(hash[other_project.id]).to eq(Gitlab::Access::MASTER) + end + end + + context 'projects shared with groups the user is a member of' do + let(:group) { create(:group) } + let(:other_project) { create(:empty_project) } + let!(:project_group_link) { create(:project_group_link, project: other_project, group: group, group_access: Gitlab::Access::GUEST) } + + before do + group.add_master(user) + end + + it 'includes the project with the right access level' do + expect(hash[other_project.id]).to eq(Gitlab::Access::GUEST) + end + end + + context 'projects shared with subgroups of groups the user is a member of' do + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + let(:other_project) { create(:empty_project) } + let!(:project_group_link) { create(:project_group_link, project: other_project, group: nested_group, group_access: Gitlab::Access::DEVELOPER) } + + before do + group.add_master(user) + end + + it 'includes the project with the right access level' do + expect(hash[other_project.id]).to eq(Gitlab::Access::DEVELOPER) + end + end end describe '#current_authorizations_per_project' do diff --git a/spec/support/features/rss_shared_examples.rb b/spec/support/features/rss_shared_examples.rb new file mode 100644 index 00000000000..9a3b0a731ad --- /dev/null +++ b/spec/support/features/rss_shared_examples.rb @@ -0,0 +1,23 @@ +shared_examples "an autodiscoverable RSS feed with current_user's private token" do + it "has an RSS autodiscovery link tag with current_user's private token" do + expect(page).to have_css("link[type*='atom+xml'][href*='private_token=#{Thread.current[:current_user].private_token}']", visible: false) + end +end + +shared_examples "it has an RSS button with current_user's private token" do + it "shows the RSS button with current_user's private token" do + expect(page).to have_css("a:has(.fa-rss)[href*='private_token=#{Thread.current[:current_user].private_token}']") + end +end + +shared_examples "an autodiscoverable RSS feed without a private token" do + it "has an RSS autodiscovery link tag without a private token" do + expect(page).to have_css("link[type*='atom+xml']:not([href*='private_token'])", visible: false) + end +end + +shared_examples "it has an RSS button without a private token" do + it "shows the RSS button without a private token" do + expect(page).to have_css("a:has(.fa-rss):not([href*='private_token'])") + end +end diff --git a/spec/support/matchers/gitaly_matchers.rb b/spec/support/matchers/gitaly_matchers.rb new file mode 100644 index 00000000000..d7a53820684 --- /dev/null +++ b/spec/support/matchers/gitaly_matchers.rb @@ -0,0 +1,3 @@ +RSpec::Matchers.define :post_receive_request_with_repo_path do |path| + match { |actual| actual.repository.path == path } +end diff --git a/spec/support/project_features_apply_to_issuables_shared_examples.rb b/spec/support/project_features_apply_to_issuables_shared_examples.rb index 4621d17549b..f8b7d0527ba 100644 --- a/spec/support/project_features_apply_to_issuables_shared_examples.rb +++ b/spec/support/project_features_apply_to_issuables_shared_examples.rb @@ -18,7 +18,7 @@ shared_examples 'project features apply to issuables' do |klass| before do _ = issuable - login_as(user) + login_as(user) if user visit path end diff --git a/spec/support/update_invalid_issuable.rb b/spec/support/update_invalid_issuable.rb index f984ac7bfa7..365c34448ac 100644 --- a/spec/support/update_invalid_issuable.rb +++ b/spec/support/update_invalid_issuable.rb @@ -33,7 +33,7 @@ shared_examples 'update invalid issuable' do |klass| end it 'renders json error message when format is json' do - params.merge!(format: "json") + params[:format] = "json" put :update, params diff --git a/spec/views/ci/status/_badge.html.haml_spec.rb b/spec/views/ci/status/_badge.html.haml_spec.rb new file mode 100644 index 00000000000..c62450fb8e2 --- /dev/null +++ b/spec/views/ci/status/_badge.html.haml_spec.rb @@ -0,0 +1,89 @@ +require 'spec_helper' + +describe 'ci/status/_badge', :view do + let(:user) { create(:user) } + let(:project) { create(:empty_project, :private) } + let(:pipeline) { create(:ci_pipeline, project: project) } + + context 'when rendering status for build' do + let(:build) do + create(:ci_build, :success, pipeline: pipeline) + end + + context 'when user has ability to see details' do + before do + project.add_developer(user) + end + + it 'has link to build details page' do + details_path = namespace_project_build_path( + project.namespace, project, build) + + render_status(build) + + expect(rendered).to have_link 'passed', href: details_path + end + end + + context 'when user do not have ability to see build details' do + before do + render_status(build) + end + + it 'contains build status text' do + expect(rendered).to have_content 'passed' + end + + it 'does not contain links' do + expect(rendered).not_to have_link 'passed' + end + end + end + + context 'when rendering status for external job' do + context 'when user has ability to see commit status details' do + before do + project.add_developer(user) + end + + context 'status has external target url' do + before do + external_job = create(:generic_commit_status, + status: :running, + pipeline: pipeline, + target_url: 'http://gitlab.com') + + render_status(external_job) + end + + it 'contains valid commit status text' do + expect(rendered).to have_content 'running' + end + + it 'has link to external status page' do + expect(rendered).to have_link 'running', href: 'http://gitlab.com' + end + end + + context 'status do not have external target url' do + before do + external_job = create(:generic_commit_status, status: :canceled) + + render_status(external_job) + end + + it 'contains valid commit status text' do + expect(rendered).to have_content 'canceled' + end + + it 'has link to external status page' do + expect(rendered).not_to have_link 'canceled' + end + end + end + end + + def render_status(resource) + render 'ci/status/badge', status: resource.detailed_status(user) + end +end diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb index b6f6e7b7a2b..ec78ac30593 100644 --- a/spec/views/projects/builds/show.html.haml_spec.rb +++ b/spec/views/projects/builds/show.html.haml_spec.rb @@ -209,6 +209,10 @@ describe 'projects/builds/show', :view do it 'does not show retry button' do expect(rendered).not_to have_link('Retry') end + + it 'does not show New issue button' do + expect(rendered).not_to have_link('New issue') + end end context 'when job is not running' do @@ -220,6 +224,23 @@ describe 'projects/builds/show', :view do it 'shows retry button' do expect(rendered).to have_link('Retry') end + + context 'if build passed' do + it 'does not show New issue button' do + expect(rendered).not_to have_link('New issue') + end + end + + context 'if build failed' do + before do + build.status = 'failed' + render + end + + it 'shows New issue button' do + expect(rendered).to have_link('New issue') + end + end end describe 'commit title in sidebar' do @@ -248,4 +269,25 @@ describe 'projects/builds/show', :view do expect(rendered).to have_css('.js-build-value', visible: false, text: 'TRIGGER_VALUE_2') end end + + describe 'New issue button' do + before do + build.status = 'failed' + render + end + + it 'links to issues/new with the title and description filled in' do + title = "Build Failed ##{build.id}" + build_url = namespace_project_build_url(project.namespace, project, build) + href = new_namespace_project_issue_path( + project.namespace, + project, + issue: { + title: title, + description: build_url + } + ) + expect(rendered).to have_link('New issue', href: href) + end + end end diff --git a/vendor/assets/javascripts/jquery.highlight.js b/vendor/assets/javascripts/jquery.highlight.js deleted file mode 100644 index 7a67cf99844..00000000000 --- a/vendor/assets/javascripts/jquery.highlight.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - -highlight v3 - -Highlights arbitrary terms. - -<http://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html> - -MIT license. - -Johann Burkard -<http://johannburkard.de> -<mailto:jb@eaio.com> - -*/ - -jQuery.fn.highlight = function(pat) { - function innerHighlight(node, pat) { - var skip = 0; - if (node.nodeType == 3) { - var pos = node.data.toUpperCase().indexOf(pat); - if (pos >= 0) { - var spannode = document.createElement('span'); - spannode.className = 'highlight_word'; - var middlebit = node.splitText(pos); - var endbit = middlebit.splitText(pat.length); - var middleclone = middlebit.cloneNode(true); - spannode.appendChild(middleclone); - middlebit.parentNode.replaceChild(spannode, middlebit); - skip = 1; - } - } - else if (node.nodeType == 1 && node.childNodes && !/(script|style)/i.test(node.tagName)) { - for (var i = 0; i < node.childNodes.length; ++i) { - i += innerHighlight(node.childNodes[i], pat); - } - } - return skip; - } - return this.each(function() { - innerHighlight(this, pat.toUpperCase()); - }); -}; - -jQuery.fn.removeHighlight = function() { - return this.find("span.highlight").each(function() { - this.parentNode.firstChild.nodeName; - with (this.parentNode) { - replaceChild(this.firstChild, this); - normalize(); - } - }).end(); -}; diff --git a/yarn.lock b/yarn.lock index cb4ef36119f..fd8d67a6b0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25,7 +25,7 @@ acorn-jsx@^3.0.0: dependencies: acorn "^3.0.4" -acorn@4.0.4, acorn@^4.0.3, acorn@^4.0.4: +acorn@4.0.4, acorn@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.4.tgz#17a8d6a7a6c4ef538b814ec9abac2779293bf30a" @@ -33,7 +33,7 @@ acorn@^3.0.4: version "3.3.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" -acorn@^4.0.11: +acorn@^4.0.11, acorn@^4.0.3: version "4.0.11" resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.11.tgz#edcda3bd937e7556410d42ed5860f67399c794c0" @@ -2639,10 +2639,6 @@ jodid25519@^1.0.0: dependencies: jsbn "~0.1.0" -"jquery-ui@git+https://github.com/jquery/jquery-ui#1.11.4": - version "1.11.4" - resolved "git+https://github.com/jquery/jquery-ui#d6713024e16de90ea71dc0544ba34e1df01b4d8a" - jquery-ujs@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/jquery-ujs/-/jquery-ujs-1.2.1.tgz#6ee75b1ef4e9ac95e7124f8d71f7d351f5548e92" @@ -3566,6 +3562,10 @@ raw-body@~2.2.0: iconv-lite "0.4.15" unpipe "1.0.0" +raw-loader@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa" + rc@~1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/rc/-/rc-1.1.6.tgz#43651b76b6ae53b5c802f1151fa3fc3b059969c9" |