diff --git a/.gitignore b/.gitignore index ce6a363fe35fe4c328b28c79407bc424681116a5..1bf9a47aef6de8a156e6ffb0e34c87de23efc445 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ /config/secrets.yml /config/sidekiq.yml /coverage/* +/coverage-javascript/ /db/*.sqlite3 /db/*.sqlite3-journal /db/data.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 741429320650e7c0ef9ae9b2a23f145e19d4645e..3ecccc4b4d589276701dbabec09684a3e3715dc8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,7 +1,7 @@ -image: "ruby:2.1" +image: "ruby:2.3.1" cache: - key: "ruby21" + key: "ruby-231" paths: - vendor/apt - vendor/ruby @@ -140,57 +140,57 @@ spinach 7 10: *spinach-knapsack spinach 8 10: *spinach-knapsack spinach 9 10: *spinach-knapsack -# Execute all testing suites against Ruby 2.3 -.ruby-23: &ruby-23 - image: "ruby:2.3" +# Execute all testing suites against Ruby 2.1 +.ruby-21: &ruby-21 + image: "ruby:2.1" <<: *use-db only: - master cache: - key: "ruby-23" + key: "ruby21" paths: - vendor/apt - vendor/ruby -.rspec-knapsack-ruby23: &rspec-knapsack-ruby23 +.rspec-knapsack-ruby21: &rspec-knapsack-ruby21 <<: *rspec-knapsack - <<: *ruby-23 + <<: *ruby-21 -.spinach-knapsack-ruby23: &spinach-knapsack-ruby23 +.spinach-knapsack-ruby21: &spinach-knapsack-ruby21 <<: *spinach-knapsack - <<: *ruby-23 + <<: *ruby-21 -rspec 0 20 ruby23: *rspec-knapsack-ruby23 -rspec 1 20 ruby23: *rspec-knapsack-ruby23 -rspec 2 20 ruby23: *rspec-knapsack-ruby23 -rspec 3 20 ruby23: *rspec-knapsack-ruby23 -rspec 4 20 ruby23: *rspec-knapsack-ruby23 -rspec 5 20 ruby23: *rspec-knapsack-ruby23 -rspec 6 20 ruby23: *rspec-knapsack-ruby23 -rspec 7 20 ruby23: *rspec-knapsack-ruby23 -rspec 8 20 ruby23: *rspec-knapsack-ruby23 -rspec 9 20 ruby23: *rspec-knapsack-ruby23 -rspec 10 20 ruby23: *rspec-knapsack-ruby23 -rspec 11 20 ruby23: *rspec-knapsack-ruby23 -rspec 12 20 ruby23: *rspec-knapsack-ruby23 -rspec 13 20 ruby23: *rspec-knapsack-ruby23 -rspec 14 20 ruby23: *rspec-knapsack-ruby23 -rspec 15 20 ruby23: *rspec-knapsack-ruby23 -rspec 16 20 ruby23: *rspec-knapsack-ruby23 -rspec 17 20 ruby23: *rspec-knapsack-ruby23 -rspec 18 20 ruby23: *rspec-knapsack-ruby23 -rspec 19 20 ruby23: *rspec-knapsack-ruby23 +rspec 0 20 ruby21: *rspec-knapsack-ruby21 +rspec 1 20 ruby21: *rspec-knapsack-ruby21 +rspec 2 20 ruby21: *rspec-knapsack-ruby21 +rspec 3 20 ruby21: *rspec-knapsack-ruby21 +rspec 4 20 ruby21: *rspec-knapsack-ruby21 +rspec 5 20 ruby21: *rspec-knapsack-ruby21 +rspec 6 20 ruby21: *rspec-knapsack-ruby21 +rspec 7 20 ruby21: *rspec-knapsack-ruby21 +rspec 8 20 ruby21: *rspec-knapsack-ruby21 +rspec 9 20 ruby21: *rspec-knapsack-ruby21 +rspec 10 20 ruby21: *rspec-knapsack-ruby21 +rspec 11 20 ruby21: *rspec-knapsack-ruby21 +rspec 12 20 ruby21: *rspec-knapsack-ruby21 +rspec 13 20 ruby21: *rspec-knapsack-ruby21 +rspec 14 20 ruby21: *rspec-knapsack-ruby21 +rspec 15 20 ruby21: *rspec-knapsack-ruby21 +rspec 16 20 ruby21: *rspec-knapsack-ruby21 +rspec 17 20 ruby21: *rspec-knapsack-ruby21 +rspec 18 20 ruby21: *rspec-knapsack-ruby21 +rspec 19 20 ruby21: *rspec-knapsack-ruby21 -spinach 0 10 ruby23: *spinach-knapsack-ruby23 -spinach 1 10 ruby23: *spinach-knapsack-ruby23 -spinach 2 10 ruby23: *spinach-knapsack-ruby23 -spinach 3 10 ruby23: *spinach-knapsack-ruby23 -spinach 4 10 ruby23: *spinach-knapsack-ruby23 -spinach 5 10 ruby23: *spinach-knapsack-ruby23 -spinach 6 10 ruby23: *spinach-knapsack-ruby23 -spinach 7 10 ruby23: *spinach-knapsack-ruby23 -spinach 8 10 ruby23: *spinach-knapsack-ruby23 -spinach 9 10 ruby23: *spinach-knapsack-ruby23 +spinach 0 10 ruby21: *spinach-knapsack-ruby21 +spinach 1 10 ruby21: *spinach-knapsack-ruby21 +spinach 2 10 ruby21: *spinach-knapsack-ruby21 +spinach 3 10 ruby21: *spinach-knapsack-ruby21 +spinach 4 10 ruby21: *spinach-knapsack-ruby21 +spinach 5 10 ruby21: *spinach-knapsack-ruby21 +spinach 6 10 ruby21: *spinach-knapsack-ruby21 +spinach 7 10 ruby21: *spinach-knapsack-ruby21 +spinach 8 10 ruby21: *spinach-knapsack-ruby21 +spinach 9 10 ruby21: *spinach-knapsack-ruby21 # Other generic tests @@ -224,7 +224,22 @@ teaspoon: stage: test <<: *use-db script: + - curl --silent --location https://deb.nodesource.com/setup_6.x | bash - + - apt-get install --assume-yes nodejs + - npm install --global istanbul - teaspoon + artifacts: + name: coverage-javascript + expire_in: 31d + paths: + - coverage-javascript/default/ + +lint-doc: + stage: test + image: "phusion/baseimage:latest" + before_script: [] + script: + - scripts/lint-doc.sh bundler:audit: stage: test @@ -271,10 +286,12 @@ pages: stage: pages dependencies: - coverage + - teaspoon script: - mv public/ .public/ - mkdir public/ - mv coverage public/coverage-ruby + - mv coverage-javascript/default/ public/coverage-javascript/ artifacts: paths: - public diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000000000000000000000000000000000000..bd5ac22132c4131ced4fc24cad666f3e304bb585 --- /dev/null +++ b/.mailmap @@ -0,0 +1,35 @@ +# +# This list is used by git-shortlog to make contributions from the +# same person appearing to be so. +# + +Achilleas Pipinellis +Achilleas Pipinellis +Dmitriy Zaporozhets +Dmitriy Zaporozhets +Douwe Maan +Douwe Maan +Grzegorz Bizon +Grzegorz Bizon +Jacob Vosmaer +Jacob Vosmaer Jacob Vosmaer (GitLab) +Jacob Schatz +Jacob Schatz +Jacob Schatz +James Lopez +James Lopez +Kamil Trzciński +Marin Jankovski +Phil Hughes +Rémy Coutable +Robert Schilling +Robert Schilling +Robert Speicher +Stan Hu +Stan Hu +Stan Hu +Stan Hu stanhu +Sytse Sijbrandij +Sytse Sijbrandij +Sytse Sijbrandij +Sytse Sijbrandij dosire diff --git a/.rubocop.yml b/.rubocop.yml index c6d5ca0089c52e6a022de18ec485dccf2b19678e..1942b6e8126053fc7652ae0b1dbc24d0a6654d99 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -149,7 +149,7 @@ Style/EmptyLinesAroundAccessModifier: # Keeps track of empty lines around block bodies. Style/EmptyLinesAroundBlockBody: - Enabled: false + Enabled: true # Keeps track of empty lines around class bodies. Style/EmptyLinesAroundClassBody: @@ -161,7 +161,7 @@ Style/EmptyLinesAroundModuleBody: # Keeps track of empty lines around method bodies. Style/EmptyLinesAroundMethodBody: - Enabled: false + Enabled: true # Avoid the use of END blocks. Style/EndBlock: diff --git a/.ruby-version b/.ruby-version index ebf14b46981c4134412e7deaef0ccdc719a195d4..2bf1c1ccf363acd53eaf92ef33a7f11f5f4557c2 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.1.8 +2.3.1 diff --git a/CHANGELOG b/CHANGELOG index 92ddf3eb33856d1d185f06aff8bad2ee2419c2d0..21219e98ab29539f83c54411c95a43b8b3f659f0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,25 +1,40 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.11.0 (unreleased) + - Remove the http_parser.rb dependency by removing the tinder gem. !5758 (tbalthazar) - Fix don't pass a local variable called `i` to a partial. !20510 (herminiotorres) - Fix rename `add_users_into_project` and `projects_ids`. !20512 (herminiotorres) - Fix the title of the toggle dropdown button. !5515 (herminiotorres) + - Rename `markdown_preview` routes to `preview_markdown`. (Christopher Bartz) + - Update to Ruby 2.3.1. !4948 - Improve diff performance by eliminating redundant checks for text blobs + - Ensure that branch names containing escapable characters (e.g. %20) aren't unescaped indiscriminately. !5770 (ewiltshi) - Convert switch icon into icon font (ClemMakesApps) + - API: Endpoints for enabling and disabling deploy keys + - API: List access requests, request access, approve, and deny access requests to a project or a group. !4833 + - Use long options for curl examples in documentation !5703 (winniehell) - Remove magic comments (`# encoding: UTF-8`) from Ruby files. !5456 (winniehell) - Add support for relative links starting with ./ or / to RelativeLinkFilter (winniehell) - Ignore URLs starting with // in Markdown links !5677 (winniehell) - Fix CI status icon link underline (ClemMakesApps) - The Repository class is now instrumented + - Fix filter label tooltip HTML rendering (ClemMakesApps) - Cache the commit author in RequestStore to avoid extra lookups in PostReceive - Expand commit message width in repo view (ClemMakesApps) - Cache highlighted diff lines for merge requests + - Pre-create all builds for a Pipeline when the new Pipeline is created !5295 - Fix of 'Commits being passed to custom hooks are already reachable when using the UI' + - Show member roles to all users on members page + - Project.visible_to_user is instrumented again + - Fix awardable button mutuality loading spinners (ClemMakesApps) - Add support for using RequestStore within Sidekiq tasks via SIDEKIQ_REQUEST_STORE env variable - Optimize maximum user access level lookup in loading of notes - Add "No one can push" as an option for protected branches. !5081 - Improve performance of AutolinkFilter#text_parse by using XPath + - Add experimental Redis Sentinel support !1877 + - Fix branches page dropdown sort initial state (ClemMakesApps) - Environments have an url to link to + - Various redundant database indexes have been removed - Update `timeago` plugin to use multiple string/locale settings - Remove unused images (ClemMakesApps) - Limit git rev-list output count to one in forced push check @@ -32,10 +47,15 @@ v 8.11.0 (unreleased) - Remove delay when hitting "Reply..." button on page with a lot of discussions - Retrieve rendered HTML from cache in one request - Fix renaming repository when name contains invalid chararacters under project settings + - Upgrade Grape from 0.13.0 to 0.15.0. !4601 + - Trigram indexes for the "ci_runners" table have been removed to speed up UPDATE queries - Fix devise deprecation warnings. + - Update version_sorter and use new interface for faster tag sorting - Optimize checking if a user has read access to a list of issues !5370 + - Store all DB secrets in secrets.yml, under descriptive names !5274 - Nokogiri's various parsing methods are now instrumented - Add simple identifier to public SSH keys (muteor) + - Admin page now references docs instead of a specific file !5600 (AnAverageHuman) - Add a way to send an email and create an issue based on private personal token. Find the email address from issues page. !3363 - Fix filter input alignment (ClemMakesApps) - Include old revision in merge request update hooks (Ben Boeckel) @@ -44,6 +64,7 @@ v 8.11.0 (unreleased) - Document that webhook secret token is sent in X-Gitlab-Token HTTP header !5664 (lycoperdon) - Gitlab::Highlight is now instrumented - All created issues, API or WebUI, can be submitted to Akismet for spam check !5333 + - Allow users to import cross-repository pull requests from GitHub - The overhead of instrumented method calls has been reduced - Remove `search_id` of labels dropdown filter to fix 'Missleading URI for labels in Merge Requests and Issues view'. !5368 (Scott Le) - Load project invited groups and members eagerly in `ProjectTeam#fetch_members` @@ -59,6 +80,7 @@ v 8.11.0 (unreleased) - Add GitLab Workhorse version to admin dashboard (Katarzyna Kobierska Ula Budziszewska) - Allow branch names ending with .json for graph and network page !5579 (winniehell) - Add the `sprockets-es6` gem + - Improve OAuth2 client documentation (muteor) - Multiple trigger variables show in separate lines (Katarzyna Kobierska Ula Budziszewska) - Profile requests when a header is passed - Avoid calculation of line_code and position for _line partial when showing diff notes on discussion tab. @@ -73,12 +95,22 @@ v 8.11.0 (unreleased) - Sensible state specific default sort order for issues and merge requests !5453 (tomb0y) - Fix RequestProfiler::Middleware error when code is reloaded in development - Catch what warden might throw when profiling requests to re-throw it + - Avoid commit lookup on diff_helper passing existing local variable to the helper method - Add description to new_issue email and new_merge_request_email in text/plain content type. !5663 (dixpac) - Speed up and reduce memory usage of Commit#repo_changes, Repository#expire_avatar_cache and IrkerWorker - Add unfold links for Side-by-Side view. !5415 (Tim Masliuchenko) - Adds support for pending invitation project members importing projects - -v 8.10.5 (unreleased) + - Update devise initializer to turn on changed password notification emails. !5648 (tombell) + - Avoid to show the original password field when password is automatically set. !5712 (duduribeiro) + - Fix importing GitLab projects with an invalid MR source project + - Sort folders with submodules in Files view !5521 + - Each `File::exists?` replaced to `File::exist?` because of deprecate since ruby version 2.2.0 + - Add auto-completition in pipeline (Katarzyna Kobierska Ula Budziszewska) + +v 8.10.5 + - Add a data migration to fix some missing timestamps in the members table. !5670 + - Revert the "Defend against 'Host' header injection" change in the source NGINX templates. !5706 + - Cache project count for 5 minutes to reduce DB load. !5746 & !5754 v 8.10.4 - Don't close referenced upstream issues from a forked project. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d94673e82cef5cbd22c7b9b2945a970dec19e655..fbc8e15bebfbd1cf468dec8784777c9e06afb34c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -465,6 +465,7 @@ merge request: - multi-line method chaining style **Option B**: dot `.` on previous line - string literal quoting style **Option A**: single quoted by default 1. [Rails](https://github.com/bbatsov/rails-style-guide) +1. [Newlines styleguide][newlines-styleguide] 1. [Testing](doc/development/testing.md) 1. [JavaScript (ES6)](https://github.com/airbnb/javascript) 1. [JavaScript (ES5)](https://github.com/airbnb/javascript/tree/master/es5) @@ -537,6 +538,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor [rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming [doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide" [scss-styleguide]: doc/development/scss_styleguide.md "SCSS styleguide" +[newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide" [gitlab-design]: https://gitlab.com/gitlab-org/gitlab-design [free Antetype viewer (Mac OSX only)]: https://itunes.apple.com/us/app/antetype-viewer/id824152298?mt=12 [`gitlab8.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/current/ diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 15a279981720791464e46ab21ae96b3c1c65c3b6..619b537668489eba5ff985e81afa2c1228281818 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -3.3.0 +3.3.3 diff --git a/Gemfile b/Gemfile index 8646daf7fcc78e6fd977d5c1240612717cdcd13d..b90f9aa6479ad99a38620e9117646a7409f1d6f7 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source 'https://rubygems.org' -gem 'rails', '4.2.7' +gem 'rails', '4.2.7.1' gem 'rails-deprecated_sanitizer', '~> 1.0.3' # Responders respond_to and respond_with @@ -74,7 +74,7 @@ gem 'gollum-rugged_adapter', '~> 0.4.2', require: false gem 'github-linguist', '~> 4.7.0', require: 'linguist' # API -gem 'grape', '~> 0.13.0' +gem 'grape', '~> 0.15.0' gem 'grape-entity', '~> 0.4.2' gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' @@ -164,7 +164,7 @@ gem 'settingslogic', '~> 2.0.9' # Misc -gem 'version_sorter', '~> 2.0.0' +gem 'version_sorter', '~> 2.1.0' # Cache gem 'redis-rails', '~> 4.0.0' @@ -173,9 +173,6 @@ gem 'redis-rails', '~> 4.0.0' gem 'redis', '~> 3.2' gem 'connection_pool', '~> 2.0' -# Campfire integration -gem 'tinder', '~> 1.10.0' - # HipChat integration gem 'hipchat', '~> 1.5.0' diff --git a/Gemfile.lock b/Gemfile.lock index cad3bebab71e890f440e6b2fb300fdac1f67354c..048a2decb1f466fa805cefe7f659c072cb553629 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,34 +3,34 @@ GEM specs: RedCloth (4.3.2) ace-rails-ap (4.0.2) - actionmailer (4.2.7) - actionpack (= 4.2.7) - actionview (= 4.2.7) - activejob (= 4.2.7) + actionmailer (4.2.7.1) + actionpack (= 4.2.7.1) + actionview (= 4.2.7.1) + activejob (= 4.2.7.1) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.7) - actionview (= 4.2.7) - activesupport (= 4.2.7) + actionpack (4.2.7.1) + actionview (= 4.2.7.1) + activesupport (= 4.2.7.1) rack (~> 1.6) rack-test (~> 0.6.2) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (4.2.7) - activesupport (= 4.2.7) + actionview (4.2.7.1) + activesupport (= 4.2.7.1) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - activejob (4.2.7) - activesupport (= 4.2.7) + activejob (4.2.7.1) + activesupport (= 4.2.7.1) globalid (>= 0.3.0) - activemodel (4.2.7) - activesupport (= 4.2.7) + activemodel (4.2.7.1) + activesupport (= 4.2.7.1) builder (~> 3.1) - activerecord (4.2.7) - activemodel (= 4.2.7) - activesupport (= 4.2.7) + activerecord (4.2.7.1) + activemodel (= 4.2.7.1) + activesupport (= 4.2.7.1) arel (~> 6.0) activerecord-session_store (1.0.0) actionpack (>= 4.0, < 5.1) @@ -38,7 +38,7 @@ GEM multi_json (~> 1.11, >= 1.11.2) rack (>= 1.5.2, < 3) railties (>= 4.0, < 5.1) - activesupport (4.2.7) + activesupport (4.2.7.1) i18n (~> 0.7) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) @@ -311,7 +311,7 @@ GEM omniauth (~> 1.0) pyu-ruby-sasl (~> 0.0.3.1) rubyntlm (~> 0.3) - globalid (0.3.6) + globalid (0.3.7) activesupport (>= 4.1.0) gollum-grit_adapter (1.0.1) gitlab-grit (~> 2.7, >= 2.7.1) @@ -330,7 +330,7 @@ GEM json multi_json request_store (>= 1.0) - grape (0.13.0) + grape (0.15.0) activesupport builder hashie (>= 2.1.0) @@ -359,7 +359,6 @@ GEM activesupport (>= 2) nokogiri (~> 1.4) htmlentities (4.3.4) - http_parser.rb (0.5.3) httparty (0.13.7) json (~> 1.8) multi_xml (>= 0.5.2) @@ -543,16 +542,16 @@ GEM rack rack-test (0.6.3) rack (>= 1.0) - rails (4.2.7) - actionmailer (= 4.2.7) - actionpack (= 4.2.7) - actionview (= 4.2.7) - activejob (= 4.2.7) - activemodel (= 4.2.7) - activerecord (= 4.2.7) - activesupport (= 4.2.7) + rails (4.2.7.1) + actionmailer (= 4.2.7.1) + actionpack (= 4.2.7.1) + actionview (= 4.2.7.1) + activejob (= 4.2.7.1) + activemodel (= 4.2.7.1) + activerecord (= 4.2.7.1) + activesupport (= 4.2.7.1) bundler (>= 1.3.0, < 2.0) - railties (= 4.2.7) + railties (= 4.2.7.1) sprockets-rails rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) @@ -562,9 +561,9 @@ GEM rails-deprecated_sanitizer (>= 1.0.1) rails-html-sanitizer (1.0.3) loofah (~> 2.0) - railties (4.2.7) - actionpack (= 4.2.7) - activesupport (= 4.2.7) + railties (4.2.7.1) + actionpack (= 4.2.7.1) + activesupport (= 4.2.7.1) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (2.1.0) @@ -696,7 +695,6 @@ GEM redis-namespace (>= 1.5.2) rufus-scheduler (>= 2.0.24) sidekiq (>= 4.0.0) - simple_oauth (0.1.9) simplecov (0.12.0) docile (~> 1.1.0) json (>= 1.8, < 3) @@ -766,21 +764,8 @@ GEM tilt (2.0.5) timecop (0.8.1) timfel-krb5-auth (0.8.3) - tinder (1.10.1) - eventmachine (~> 1.0) - faraday (~> 0.9.0) - faraday_middleware (~> 0.9) - hashie (>= 1.0) - json (~> 1.8.0) - mime-types - multi_json (~> 1.7) - twitter-stream (~> 0.1) turbolinks (2.5.3) coffee-rails - twitter-stream (0.1.16) - eventmachine (>= 0.12.8) - http_parser.rb (~> 0.5.1) - simple_oauth (~> 0.1.4) tzinfo (1.2.2) thread_safe (~> 0.1) u2f (0.2.1) @@ -805,7 +790,7 @@ GEM validates_hostname (1.0.5) activerecord (>= 3.0) activesupport (>= 3.0) - version_sorter (2.0.0) + version_sorter (2.1.0) virtus (1.0.5) axiom-types (~> 0.1) coercible (~> 1.0) @@ -907,7 +892,7 @@ DEPENDENCIES gollum-lib (~> 4.2) gollum-rugged_adapter (~> 0.4.2) gon (~> 6.1.0) - grape (~> 0.13.0) + grape (~> 0.15.0) grape-entity (~> 0.4.2) gssapi hamlit (~> 2.5) @@ -962,7 +947,7 @@ DEPENDENCIES rack-attack (~> 4.3.1) rack-cors (~> 0.4.0) rack-oauth2 (~> 1.2.1) - rails (= 4.2.7) + rails (= 4.2.7.1) rails-deprecated_sanitizer (~> 1.0.3) rainbow (~> 2.1.0) rblineprof (~> 0.3.6) @@ -1014,7 +999,6 @@ DEPENDENCIES teaspoon-jasmine (~> 2.2.0) test_after_commit (~> 0.4.2) thin (~> 1.7.0) - tinder (~> 1.10.0) turbolinks (~> 2.5.0) u2f (~> 0.2.1) uglifier (~> 2.7.2) @@ -1023,7 +1007,7 @@ DEPENDENCIES unicorn (~> 4.9.0) unicorn-worker-killer (~> 0.4.2) validates_hostname (~> 1.0.0) - version_sorter (~> 2.0.0) + version_sorter (~> 2.1.0) virtus (~> 1.0.1) vmstat (~> 2.1.1) web-console (~> 2.0) diff --git a/VERSION b/VERSION index efbb18d78447052bd35e1ff4e2cb7c599745f4de..5b42822dbe2c2ccdc390ad89f1800bb9a754a4e1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.11.0-ee-pre +8.11.0-rc1-ee diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index ea683b31f75a2ce1ef066984c01d70239af0fd74..2c5b83e4f1e1295a6ec0732c18873b65a690f5c0 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -161,23 +161,11 @@ $emojiButton = votesBlock.find("[data-emoji=" + mutualVote + "]").parent(); isAlreadyVoted = $emojiButton.hasClass('active'); if (isAlreadyVoted) { - this.showEmojiLoader($emojiButton); - return this.addAward(votesBlock, awardUrl, mutualVote, false, function() { - return $emojiButton.removeClass('is-loading'); - }); + this.addAward(votesBlock, awardUrl, mutualVote, false); } } }; - AwardsHandler.prototype.showEmojiLoader = function($emojiButton) { - var $loader; - $loader = $emojiButton.find('.fa-spinner'); - if (!$loader.length) { - $emojiButton.append(''); - } - return $emojiButton.addClass('is-loading'); - }; - AwardsHandler.prototype.isActive = function($emojiButton) { return $emojiButton.hasClass('active'); }; diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 67c3cc41eefe5dab2e8edba894aac8264f495681..96f7e23b8a242d47379569488d2edbd864572574 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -193,6 +193,12 @@ break; case 'projects': new NamespaceSelects(); + break; + case 'labels': + switch (path[2]) { + case 'edit': + new Labels(); + } } break; case 'dashboard': @@ -218,6 +224,7 @@ new ProjectNew(); break; case 'show': + new Star(); new ProjectNew(); new ProjectShow(); new NotificationsDropdown(); diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 288cce04f878284377e68dd0320226d9bba46509..4a6fea929c758b55d5fa382d868e6982710258d1 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -1,5 +1,5 @@ -/*= require markdown_preview */ +/*= require preview_markdown */ (function() { this.DropzoneInput = (function() { diff --git a/app/assets/javascripts/markdown_preview.js b/app/assets/javascripts/preview_markdown.js similarity index 98% rename from app/assets/javascripts/markdown_preview.js rename to app/assets/javascripts/preview_markdown.js index 18fc7bae09a6e5f65a3fbbce792b11097bde90fa..5fd7579964024c842915af9f237863d7335394f0 100644 --- a/app/assets/javascripts/markdown_preview.js +++ b/app/assets/javascripts/preview_markdown.js @@ -28,7 +28,7 @@ }; MarkdownPreview.prototype.renderMarkdown = function(text, success) { - if (!window.markdown_preview_path) { + if (!window.preview_markdown_path) { return; } if (text === this.ajaxCache.text) { @@ -36,7 +36,7 @@ } return $.ajax({ type: 'POST', - url: window.markdown_preview_path, + url: window.preview_markdown_path, data: { text: text }, diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index e160d676e35c3dffbce4d19e3cbca2c3779b6fac..55f9d4a001123f9549b31aace1063f1e85e2bdf3 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -1,5 +1,35 @@ .environments { + .commit-title { margin: 0; } + + .fa-play { + font-size: 14px; + } + + .dropdown-new { + color: $table-text-gray; + } + + .dropdown-menu { + + .fa { + margin-right: 6px; + color: $table-text-gray; + } + } + + .branch-name { + color: $gl-dark-link-color; + } +} + +.table.builds.environments { + min-width: 500px; + + .icon-container { + width: 20px; + text-align: center; + } } diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 3b1e38fc07ddd44a9d6b346759a181aa14089d1f..606459f82cd03c89102b723c5db1bacd9081ba49 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -182,6 +182,17 @@ .btn { color: inherit; } + + a.btn { + padding: 0; + + .has-tooltip { + top: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + line-height: 1.1; + } + } } .label-options-toggle { diff --git a/app/controllers/profiles/passwords_controller.rb b/app/controllers/profiles/passwords_controller.rb index c780e0983f93ae07443138cb2a42ef95828a767b..6217ec5ecef196dcb4306250d6d9543663c38b74 100644 --- a/app/controllers/profiles/passwords_controller.rb +++ b/app/controllers/profiles/passwords_controller.rb @@ -50,6 +50,7 @@ def update flash[:notice] = "Password was successfully updated. Please login with it" redirect_to new_user_session_path else + @user.reload render 'edit' end end diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb index a9f482c8787b0edad09127fcaa472fd8133a86b7..d0f5071d2cc21195e962f8a5341f8d568bb195c0 100644 --- a/app/controllers/projects/badges_controller.rb +++ b/app/controllers/projects/badges_controller.rb @@ -8,8 +8,9 @@ def build respond_to do |format| format.html { render_404 } + format.svg do - send_data(badge.data, type: badge.type, disposition: 'inline') + render 'badge', locals: { badge: badge.template } end end end diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index e926043f3ebadbcf21721efa2430daa852f23409..48fe81b0d7456b66f0e200447dea7fa9f34ed0b3 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -1,12 +1,13 @@ class Projects::BranchesController < Projects::ApplicationController include ActionView::Helpers::SanitizeHelper + include SortingHelper # Authorize before_action :require_non_empty_project before_action :authorize_download_code! before_action :authorize_push_code!, only: [:new, :create, :destroy] def index - @sort = params[:sort].presence || 'name' + @sort = params[:sort].presence || sort_value_name @branches = BranchesFinder.new(@repository, params).execute @branches = Kaminari.paginate_array(@branches).page(params[:page]) diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 553b62741a5de091ad536aa467686d2ac1794014..12195c3cbb82704fd93d8648909551a1a75e5f19 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -6,7 +6,7 @@ class Projects::BuildsController < Projects::ApplicationController def index @scope = params[:scope] - @all_builds = project.builds + @all_builds = project.builds.relevant @builds = @all_builds.order('created_at DESC') @builds = case @scope diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index fdfe7c65b7b0bce9a751dabcac64eee29e7a9b84..f44e9bb3fd7d1c3324c549c6126a5b1ae013bfe5 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -134,8 +134,8 @@ def define_note_vars end def define_status_vars - @statuses = CommitStatus.where(pipeline: pipelines) - @builds = Ci::Build.where(pipeline: pipelines) + @statuses = CommitStatus.where(pipeline: pipelines).relevant + @builds = Ci::Build.where(pipeline: pipelines).relevant end def assign_change_commit_vars(mr_source_branch) diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index 8f4548c5c89f910532ca921c12f39825780d9842..c378ad8a7c8be0bb4de238a34998b6396b1cc355 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -12,8 +12,7 @@ def index end def new - redirect_to namespace_project_deploy_keys_path(@project.namespace, - @project) + redirect_to namespace_project_deploy_keys_path(@project.namespace, @project) end def create @@ -23,8 +22,7 @@ def create if @key.valid? && @project.deploy_keys << @key log_audit_event(@key.title, action: :create) - redirect_to namespace_project_deploy_keys_path(@project.namespace, - @project) + redirect_to namespace_project_deploy_keys_path(@project.namespace, @project) else render "index" end @@ -35,8 +33,9 @@ def enable @project.deploy_keys << @key log_audit_event(@key.title, action: :create) - redirect_to namespace_project_deploy_keys_path(@project.namespace, - @project) + Projects::EnableDeployKeyService.new(@project, current_user, params).execute + + redirect_to namespace_project_deploy_keys_path(@project.namespace, @project) end def disable @@ -50,9 +49,9 @@ def disable protected def set_index_vars - @enabled_keys ||= @project.deploy_keys + @enabled_keys ||= @project.deploy_keys - @available_keys ||= accessible_keys - @enabled_keys + @available_keys ||= current_user.accessible_deploy_keys - @enabled_keys @available_project_keys ||= current_user.project_deploy_keys - @enabled_keys @available_public_keys ||= DeployKey.are_public - @enabled_keys @@ -61,10 +60,6 @@ def set_index_vars @available_public_keys -= @available_project_keys end - def accessible_keys - @accessible_keys ||= current_user.accessible_deploy_keys - end - def deploy_key_params params.require(:deploy_key).permit(:key, :title) end diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..7c21bd181dc04d579ac8798e6cbee14d49380ef2 --- /dev/null +++ b/app/controllers/projects/git_http_client_controller.rb @@ -0,0 +1,110 @@ +# This file should be identical in GitLab Community Edition and Enterprise Edition + +class Projects::GitHttpClientController < Projects::ApplicationController + include ActionController::HttpAuthentication::Basic + include KerberosSpnegoHelper + + attr_reader :user + + # Git clients will not know what authenticity token to send along + skip_before_action :verify_authenticity_token + skip_before_action :repository + before_action :authenticate_user + before_action :ensure_project_found! + + private + + def authenticate_user + if project && project.public? && download_request? + return # Allow access + end + + if allow_basic_auth? && basic_auth_provided? + login, password = user_name_and_password(request) + auth_result = Gitlab::Auth.find_for_git_client(login, password, project: project, ip: request.ip) + + if auth_result.type == :ci && download_request? + @ci = true + elsif auth_result.type == :oauth && !download_request? + # Not allowed + else + @user = auth_result.user + end + + if ci? || user + return # Allow access + end + elsif allow_kerberos_spnego_auth? && spnego_provided? + @user = find_kerberos_user + + if user + send_final_spnego_response + return # Allow access + end + end + + send_challenges + render plain: "HTTP Basic: Access denied\n", status: 401 + end + + def basic_auth_provided? + has_basic_credentials?(request) + end + + def send_challenges + challenges = [] + challenges << 'Basic realm="GitLab"' if allow_basic_auth? + challenges << spnego_challenge if allow_kerberos_spnego_auth? + headers['Www-Authenticate'] = challenges.join("\n") if challenges.any? + end + + def ensure_project_found! + render_not_found if project.blank? + end + + def project + return @project if defined?(@project) + + project_id, _ = project_id_with_suffix + if project_id.blank? + @project = nil + else + @project = Project.find_with_namespace("#{params[:namespace_id]}/#{project_id}") + end + end + + # This method returns two values so that we can parse + # params[:project_id] (untrusted input!) in exactly one place. + def project_id_with_suffix + id = params[:project_id] || '' + + %w[.wiki.git .git].each do |suffix| + if id.end_with?(suffix) + # Be careful to only remove the suffix from the end of 'id'. + # Accidentally removing it from the middle is how security + # vulnerabilities happen! + return [id.slice(0, id.length - suffix.length), suffix] + end + end + + # Something is wrong with params[:project_id]; do not pass it on. + [nil, nil] + end + + def repository + _, suffix = project_id_with_suffix + if suffix == '.wiki.git' + project.wiki.repository + else + project.repository + end + end + + def render_not_found + render plain: 'Not Found', status: :not_found + end + + def ci? + @ci.present? + end +end diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index 40a8b7940d9eb1a1e7d1b1c80cb1b26a50918886..b4373ef89efa6575ddadb01311c627d3762346e8 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -1,17 +1,6 @@ # This file should be identical in GitLab Community Edition and Enterprise Edition -class Projects::GitHttpController < Projects::ApplicationController - include ActionController::HttpAuthentication::Basic - include KerberosSpnegoHelper - - attr_reader :user - - # Git clients will not know what authenticity token to send along - skip_before_action :verify_authenticity_token - skip_before_action :repository - before_action :authenticate_user - before_action :ensure_project_found! - +class Projects::GitHttpController < Projects::GitHttpClientController # GET /foo/bar.git/info/refs?service=git-upload-pack (git pull) # GET /foo/bar.git/info/refs?service=git-receive-pack (git push) def info_refs @@ -20,9 +9,9 @@ def info_refs elsif receive_pack? && receive_pack_allowed? render_ok elsif http_blocked? - render_not_allowed + render_http_not_allowed else - render_not_found + render_denied end end @@ -31,7 +20,7 @@ def git_upload_pack if upload_pack? && upload_pack_allowed? render_ok else - render_not_found + render_denied end end @@ -40,87 +29,14 @@ def git_receive_pack if receive_pack? && receive_pack_allowed? render_ok else - render_not_found + render_denied end end private - def authenticate_user - if project && project.public? && upload_pack? - return # Allow access - end - - if allow_basic_auth? && basic_auth_provided? - login, password = user_name_and_password(request) - auth_result = Gitlab::Auth.find_for_git_client(login, password, project: project, ip: request.ip) - - if auth_result.type == :ci && upload_pack? - @ci = true - elsif auth_result.type == :oauth && !upload_pack? - # Not allowed - else - @user = auth_result.user - end - - if ci? || user - return # Allow access - end - elsif allow_kerberos_spnego_auth? && spnego_provided? - @user = find_kerberos_user - - if user - send_final_spnego_response - return # Allow access - end - end - - send_challenges - render plain: "HTTP Basic: Access denied\n", status: 401 - end - - def basic_auth_provided? - has_basic_credentials?(request) - end - - def send_challenges - challenges = [] - challenges << 'Basic realm="GitLab"' if allow_basic_auth? - challenges << spnego_challenge if allow_kerberos_spnego_auth? - headers['Www-Authenticate'] = challenges.join("\n") if challenges.any? - end - - def ensure_project_found! - render_not_found if project.blank? - end - - def project - return @project if defined?(@project) - - project_id, _ = project_id_with_suffix - if project_id.blank? - @project = nil - else - @project = Project.find_with_namespace("#{params[:namespace_id]}/#{project_id}") - end - end - - # This method returns two values so that we can parse - # params[:project_id] (untrusted input!) in exactly one place. - def project_id_with_suffix - id = params[:project_id] || '' - - %w[.wiki.git .git].each do |suffix| - if id.end_with?(suffix) - # Be careful to only remove the suffix from the end of 'id'. - # Accidentally removing it from the middle is how security - # vulnerabilities happen! - return [id.slice(0, id.length - suffix.length), suffix] - end - end - - # Something is wrong with params[:project_id]; do not pass it on. - [nil, nil] + def download_request? + upload_pack? end def upload_pack? @@ -143,47 +59,37 @@ def render_ok render json: Gitlab::Workhorse.git_http_ok(repository, user) end - def repository - _, suffix = project_id_with_suffix - if suffix == '.wiki.git' - project.wiki.repository - else - project.repository - end - end - - def render_not_found - render plain: 'Not Found', status: :not_found + def render_http_not_allowed + render plain: access_check.message, status: :forbidden end - def render_not_allowed - render plain: download_access.message, status: :forbidden - end - - def ci? - @ci.present? + def render_denied + if user && user.can?(:read_project, project) + render plain: 'Access denied', status: :forbidden + else + # Do not leak information about project existence + render_not_found + end end def upload_pack_allowed? return false unless Gitlab.config.gitlab_shell.upload_pack if user - download_access.allowed? + access_check.allowed? else ci? || project.public? end end def access - return @access if defined?(@access) - - @access = Gitlab::GitAccess.new(user, project, 'http') + @access ||= Gitlab::GitAccess.new(user, project, 'http') end - def download_access - return @download_access if defined?(@download_access) - - @download_access = access.check('git-upload-pack') + def access_check + # Use the magic string '_any' to indicate we do not know what the + # changes are. This is also what gitlab-shell does. + @access_check ||= access.check(git_command, '_any') end def http_blocked? @@ -193,8 +99,6 @@ def http_blocked? def receive_pack_allowed? return false unless Gitlab.config.gitlab_shell.receive_pack - # Skip user authorization on upload request. - # It will be done by the pre-receive hook in the repository. - user.present? + access_check.allowed? end end diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..ece49dcd92257209cda06b7775772a1898feff6b --- /dev/null +++ b/app/controllers/projects/lfs_api_controller.rb @@ -0,0 +1,94 @@ +class Projects::LfsApiController < Projects::GitHttpClientController + include LfsHelper + + before_action :require_lfs_enabled! + before_action :lfs_check_access!, except: [:deprecated] + + def batch + unless objects.present? + render_lfs_not_found + return + end + + if download_request? + render json: { objects: download_objects! } + elsif upload_request? + render json: { objects: upload_objects! } + else + raise "Never reached" + end + end + + def deprecated + render( + json: { + message: 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.', + documentation_url: "#{Gitlab.config.gitlab.url}/help", + }, + status: 501 + ) + end + + private + + def objects + @objects ||= (params[:objects] || []).to_a + end + + def existing_oids + @existing_oids ||= begin + storage_project.lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid) + end + end + + def download_objects! + objects.each do |object| + if existing_oids.include?(object[:oid]) + object[:actions] = download_actions(object) + else + object[:error] = { + code: 404, + message: "Object does not exist on the server or you don't have permissions to access it", + } + end + end + objects + end + + def upload_objects! + objects.each do |object| + object[:actions] = upload_actions(object) unless existing_oids.include?(object[:oid]) + end + objects + end + + def download_actions(object) + { + download: { + href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}", + header: { + Authorization: request.headers['Authorization'] + }.compact + } + } + end + + def upload_actions(object) + { + upload: { + href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}/#{object[:size]}", + header: { + Authorization: request.headers['Authorization'] + }.compact + } + } + end + + def download_request? + params[:operation] == 'download' + end + + def upload_request? + params[:operation] == 'upload' + end +end diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..69066cb40e671286810267fb4650699bf66aea58 --- /dev/null +++ b/app/controllers/projects/lfs_storage_controller.rb @@ -0,0 +1,92 @@ +class Projects::LfsStorageController < Projects::GitHttpClientController + include LfsHelper + + before_action :require_lfs_enabled! + before_action :lfs_check_access! + + def download + lfs_object = LfsObject.find_by_oid(oid) + unless lfs_object && lfs_object.file.exists? + render_lfs_not_found + return + end + + send_file lfs_object.file.path, content_type: "application/octet-stream" + end + + def upload_authorize + render( + json: { + StoreLFSPath: "#{Gitlab.config.lfs.storage_path}/tmp/upload", + LfsOid: oid, + LfsSize: size, + }, + content_type: 'application/json; charset=utf-8' + ) + end + + def upload_finalize + unless tmp_filename + render_lfs_forbidden + return + end + + if store_file(oid, size, tmp_filename) + head 200 + else + render plain: 'Unprocessable entity', status: 422 + end + end + + private + + def download_request? + action_name == 'download' + end + + def upload_request? + %w[upload_authorize upload_finalize].include? action_name + end + + def oid + params[:oid].to_s + end + + def size + params[:size].to_i + end + + def tmp_filename + name = request.headers['X-Gitlab-Lfs-Tmp'] + return if name.include?('/') + return unless oid.present? && name.start_with?(oid) + name + end + + def store_file(oid, size, tmp_file) + # Define tmp_file_path early because we use it in "ensure" + tmp_file_path = File.join("#{Gitlab.config.lfs.storage_path}/tmp/upload", tmp_file) + + object = LfsObject.find_or_create_by(oid: oid, size: size) + file_exists = object.file.exists? || move_tmp_file_to_storage(object, tmp_file_path) + file_exists && link_to_project(object) + ensure + FileUtils.rm_f(tmp_file_path) + end + + def move_tmp_file_to_storage(object, path) + File.open(path) do |f| + object.file = f + end + + object.file.store! + object.save + end + + def link_to_project(object) + if object && !object.projects.exists?(storage_project.id) + object.projects << storage_project + object.save + end + end +end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 0a792b165b396983066cdef81aa524e041507f83..249200a30a0cb4c7f78e44f3c3a8e910731513d1 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -161,7 +161,7 @@ def new @diff_notes_disabled = true @pipeline = @merge_request.pipeline - @statuses = @pipeline.statuses if @pipeline + @statuses = @pipeline.statuses.relevant if @pipeline @note_counts = Note.where(commit_id: @commits.map(&:id)). group(:commit_id).count @@ -402,7 +402,7 @@ def define_show_vars @commits_count = @merge_request.commits.count @pipeline = @merge_request.pipeline - @statuses = @pipeline.statuses if @pipeline + @statuses = @pipeline.statuses.relevant if @pipeline if @merge_request.locked_long_ago? @merge_request.unlock_mr diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 487963fdcd7b712721bd3640c9aae714bf6c7d56..b0c72cfe4b4fd6da6e4cb569eccb0b99ccecb359 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -19,7 +19,7 @@ def new end def create - @pipeline = Ci::CreatePipelineService.new(project, current_user, create_params).execute + @pipeline = Ci::CreatePipelineService.new(project, current_user, create_params).execute(ignore_skip_ci: true, save_on_errors: false) unless @pipeline.persisted? render 'new' return diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb index 85ba706e5cd050eeb19e074a8141611d344e765a..75dd3648e4528004eb36bc99cc125f7e407848c2 100644 --- a/app/controllers/projects/pipelines_settings_controller.rb +++ b/app/controllers/projects/pipelines_settings_controller.rb @@ -3,7 +3,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController def show @ref = params[:ref] || @project.default_branch || 'master' - @build_badge = Gitlab::Badge::Build.new(@project, @ref) + @build_badge = Gitlab::Badge::Build.new(@project, @ref).metadata end def update diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 9673bad1a787e2422bfbb441ec96253d71ce9a4a..dd5c8684a1d548305a4eb0e173cabb48ffaed38e 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -100,7 +100,7 @@ def destroy ) end - def markdown_preview + def preview_markdown text = params[:text] ext = Gitlab::ReferenceExtractor.new(@project, current_user) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 7ee58901859dd2b92a2f100a7992780bae3ab84e..ad0d510b58532a4cac19e950d5d60175f75f62ef 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -126,7 +126,7 @@ def show def destroy return access_denied! unless can?(current_user, :remove_project, @project) - ::Projects::DestroyService.new(@project, current_user, {}).pending_delete! + ::Projects::DestroyService.new(@project, current_user, {}).async_execute flash[:alert] = "Project '#{@project.name}' will be deleted." redirect_to dashboard_projects_path @@ -239,7 +239,7 @@ def toggle_star } end - def markdown_preview + def preview_markdown text = params[:text] ext = Gitlab::ReferenceExtractor.new(@project, current_user) diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb index 2160cf7a6907f1424caf490ea7bd4379d2c0d60e..aa8acbe7567585ae346b2d98ab509e91bc499ec8 100644 --- a/app/helpers/avatars_helper.rb +++ b/app/helpers/avatars_helper.rb @@ -7,8 +7,6 @@ def author_avatar(commit_or_event, options = {}) })) end - private - def user_avatar(options = {}) avatar_size = options[:size] || 16 user_name = options[:user].try(:name) || options[:user_name] diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index f3c9ea074b470da1d1b6c2e68aa7ec9c5bfe66f7..0725c3f4c56c812256d6021e3cb9978972e11633 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -109,11 +109,10 @@ def commit_for_diff(diff_file) end end - def diff_file_html_data(project, diff_file) - commit = commit_for_diff(diff_file) + def diff_file_html_data(project, diff_file_path, diff_commit_id) { blob_diff_path: namespace_project_blob_diff_path(project.namespace, project, - tree_join(commit.id, diff_file.file_path)), + tree_join(diff_commit_id, diff_file_path)), view: diff_view } end diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb new file mode 100644 index 0000000000000000000000000000000000000000..eb651e3687eb64eb8d0eaf66e43faf7ad6e78128 --- /dev/null +++ b/app/helpers/lfs_helper.rb @@ -0,0 +1,67 @@ +module LfsHelper + def require_lfs_enabled! + return if Gitlab.config.lfs.enabled + + render( + json: { + message: 'Git LFS is not enabled on this GitLab server, contact your admin.', + documentation_url: "#{Gitlab.config.gitlab.url}/help", + }, + status: 501 + ) + end + + def lfs_check_access! + return if download_request? && lfs_download_access? + return if upload_request? && lfs_upload_access? + + if project.public? || (user && user.can?(:read_project, project)) + render_lfs_forbidden + else + render_lfs_not_found + end + end + + def lfs_download_access? + project.public? || ci? || (user && user.can?(:download_code, project)) + end + + def lfs_upload_access? + user && user.can?(:push_code, project) + end + + def render_lfs_forbidden + render( + json: { + message: 'Access forbidden. Check your access level.', + documentation_url: "#{Gitlab.config.gitlab.url}/help", + }, + content_type: "application/vnd.git-lfs+json", + status: 403 + ) + end + + def render_lfs_not_found + render( + json: { + message: 'Not found.', + documentation_url: "#{Gitlab.config.gitlab.url}/help", + }, + content_type: "application/vnd.git-lfs+json", + status: 404 + ) + end + + def storage_project + @storage_project ||= begin + result = project + + loop do + break unless result.forked? + result = result.forked_from_project + end + + result + end + end +end diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb index ec106418f2dc4f69b051688ab30ab1dcd8fa59d4..877c77050bed0979d2814dacb549fd279f3d89e0 100644 --- a/app/helpers/members_helper.rb +++ b/app/helpers/members_helper.rb @@ -6,12 +6,6 @@ def action_member_permission(action, member) "#{action}_#{member.type.underscore}".to_sym end - def default_show_roles(member) - can?(current_user, action_member_permission(:update, member), member) || - can?(current_user, action_member_permission(:destroy, member), member) || - can?(current_user, action_member_permission(:admin, member), member.source) - end - def remove_member_message(member, user: nil) user = current_user if defined?(current_user) diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index b44be2d386b7d672a7933349276f6a03552f0391..947d1249137b5b07c4e18df5af05729158407ad5 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -4,23 +4,11 @@ module TreeHelper # # contents - A Grit::Tree object for the current tree def render_tree(tree) - # Render Folders before Files/Submodules + # Sort submodules and folders together by name ahead of files folders, files, submodules = tree.trees, tree.blobs, tree.submodules - tree = "" - - # Render folders if we have any - tree << render(partial: 'projects/tree/tree_item', collection: folders, - locals: { type: 'folder' }) if folders.present? - - # Render files if we have any - tree << render(partial: 'projects/tree/blob_item', collection: files, - locals: { type: 'file' }) if files.present? - - # Render submodules if we have any - tree << render(partial: 'projects/tree/submodule_item', - collection: submodules) if submodules.present? - + items = (folders + submodules).sort_by(&:name) + files + tree << render(partial: "projects/tree/tree_row", collection: items) if items.present? tree.html_safe end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 864af2ca3d57930738c99a8b075ee0c0b48b9c4f..7c8a8116ea7334f0585ff8f47579f0fbb52cb876 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -16,7 +16,7 @@ class Build < CommitStatus scope :with_artifacts_not_expired, ->() { with_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) } scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } - scope :manual_actions, ->() { where(when: :manual) } + scope :manual_actions, ->() { where(when: :manual).relevant } mount_uploader :artifacts_file, ArtifactUploader mount_uploader :artifacts_metadata, ArtifactUploader @@ -42,40 +42,35 @@ def create_from(build) end def retry(build, user = nil) - new_build = Ci::Build.new(status: 'pending') - new_build.ref = build.ref - new_build.tag = build.tag - new_build.options = build.options - new_build.commands = build.commands - new_build.tag_list = build.tag_list - new_build.project = build.project - new_build.pipeline = build.pipeline - new_build.name = build.name - new_build.allow_failure = build.allow_failure - new_build.stage = build.stage - new_build.stage_idx = build.stage_idx - new_build.trigger_request = build.trigger_request - new_build.yaml_variables = build.yaml_variables - new_build.when = build.when - new_build.user = user - new_build.environment = build.environment - new_build.save + new_build = Ci::Build.create( + ref: build.ref, + tag: build.tag, + options: build.options, + commands: build.commands, + tag_list: build.tag_list, + project: build.project, + pipeline: build.pipeline, + name: build.name, + allow_failure: build.allow_failure, + stage: build.stage, + stage_idx: build.stage_idx, + trigger_request: build.trigger_request, + yaml_variables: build.yaml_variables, + when: build.when, + user: user, + environment: build.environment, + status_event: 'enqueue' + ) MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build) new_build end end - state_machine :status, initial: :pending do + state_machine :status do after_transition pending: :running do |build| build.execute_hooks end - # We use around_transition to create builds for next stage as soon as possible, before the `after_*` is executed - around_transition any => [:success, :failed, :canceled] do |build, block| - block.call - build.pipeline.create_next_builds(build) if build.pipeline - end - after_transition any => [:success, :failed, :canceled] do |build| build.update_coverage build.execute_hooks @@ -107,7 +102,7 @@ def playable? def play(current_user = nil) # Try to queue a current build - if self.queue + if self.enqueue self.update(user: current_user) self else @@ -462,7 +457,7 @@ def predefined_variables def build_attributes_from_config return {} unless pipeline.config_processor - + pipeline.config_processor.build_attributes(name) end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index bce6a992af6220e3bdf2c452fdb89506597720cb..8cfba92ae9b33236c84452558309441559fda40d 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -13,13 +13,51 @@ class Pipeline < ActiveRecord::Base has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest', foreign_key: :commit_id validates_presence_of :sha + validates_presence_of :ref validates_presence_of :status validate :valid_commit_sha - # Invalidate object and save if when touched - after_touch :update_state after_save :keep_around_commits + state_machine :status, initial: :created do + event :enqueue do + transition created: :pending + transition [:success, :failed, :canceled, :skipped] => :running + end + + event :run do + transition any => :running + end + + event :skip do + transition any => :skipped + end + + event :drop do + transition any => :failed + end + + event :succeed do + transition any => :success + end + + event :cancel do + transition any => :canceled + end + + before_transition [:created, :pending] => :running do |pipeline| + pipeline.started_at = Time.now + end + + before_transition any => [:success, :failed, :canceled] do |pipeline| + pipeline.finished_at = Time.now + end + + before_transition do |pipeline| + pipeline.update_duration + end + end + # ref can't be HEAD or SHA, can only be branch/tag name scope :latest_successful_for, ->(ref = default_branch) do where(ref: ref).success.order(id: :desc).limit(1) @@ -109,37 +147,6 @@ def triggered? trigger_requests.any? end - def create_builds(user, trigger_request = nil) - ## - # We persist pipeline only if there are builds available - # - return unless config_processor - - build_builds_for_stages(config_processor.stages, user, - 'success', trigger_request) && save - end - - def create_next_builds(build) - return unless config_processor - - # don't create other builds if this one is retried - latest_builds = builds.latest - return unless latest_builds.exists?(build.id) - - # get list of stages after this build - next_stages = config_processor.stages.drop_while { |stage| stage != build.stage } - next_stages.delete(build.stage) - - # get status for all prior builds - prior_builds = latest_builds.where.not(stage: next_stages) - prior_status = prior_builds.status - - # build builds for next stage that has builds available - # and save pipeline if we have builds - build_builds_for_stages(next_stages, build.user, prior_status, - build.trigger_request) && save - end - def retried @retried ||= (statuses.order(id: :desc) - statuses.latest) end @@ -151,6 +158,14 @@ def coverage end end + def config_builds_attributes + return [] unless config_processor + + config_processor. + builds_for_ref(ref, tag?, trigger_requests.first). + sort_by { |build| build[:stage_idx] } + end + def has_warnings? builds.latest.ignored.any? end @@ -182,10 +197,6 @@ def ci_yaml_file end end - def skip_ci? - git_commit_message =~ /\[(ci skip|skip ci)\]/i if git_commit_message - end - def environments builds.where.not(environment: nil).success.pluck(:environment).uniq end @@ -207,37 +218,37 @@ def notes Note.for_commit_id(sha) end + def process! + Ci::ProcessPipelineService.new(project, user).execute(self) + end + + def build_updated + case latest_builds_status + when 'pending' then enqueue + when 'running' then run + when 'success' then succeed + when 'failed' then drop + when 'canceled' then cancel + when 'skipped' then skip + end + end + def predefined_variables [ { key: 'CI_PIPELINE_ID', value: id.to_s, public: true } ] end + def update_duration + self.duration = statuses.latest.duration + end + private - def build_builds_for_stages(stages, user, status, trigger_request) - ## - # Note that `Array#any?` implements a short circuit evaluation, so we - # build builds only for the first stage that has builds available. - # - stages.any? do |stage| - CreateBuildsService.new(self). - execute(stage, user, status, trigger_request). - any?(&:active?) - end - end - - def update_state - statuses.reload - self.status = if yaml_errors.blank? - statuses.latest.status || 'skipped' - else - 'failed' - end - self.started_at = statuses.started_at - self.finished_at = statuses.finished_at - self.duration = statuses.latest.duration - save + def latest_builds_status + return 'failed' unless yaml_errors.blank? + + statuses.latest.status || 'skipped' end def keep_around_commits diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 2d185c28809d69eced89d1057662c5c7d309d7e1..703ca90edb6982bee860ca1825bb55d908eaf170 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -5,7 +5,7 @@ class CommitStatus < ActiveRecord::Base self.table_name = 'ci_builds' belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id - belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id, touch: true + belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id belongs_to :user delegate :commit, to: :pipeline @@ -25,28 +25,36 @@ class CommitStatus < ActiveRecord::Base scope :ordered, -> { order(:name) } scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) } - state_machine :status, initial: :pending do - event :queue do - transition skipped: :pending + state_machine :status do + event :enqueue do + transition [:created, :skipped] => :pending end event :run do transition pending: :running end + event :skip do + transition [:created, :pending] => :skipped + end + event :drop do - transition [:pending, :running] => :failed + transition [:created, :pending, :running] => :failed end event :success do - transition [:pending, :running] => :success + transition [:created, :pending, :running] => :success end event :cancel do - transition [:pending, :running] => :canceled + transition [:created, :pending, :running] => :canceled end - after_transition pending: :running do |commit_status| + after_transition created: [:pending, :running] do |commit_status| + commit_status.update_attributes queued_at: Time.now + end + + after_transition [:created, :pending] => :running do |commit_status| commit_status.update_attributes started_at: Time.now end @@ -54,7 +62,18 @@ class CommitStatus < ActiveRecord::Base commit_status.update_attributes finished_at: Time.now end - after_transition [:pending, :running] => :success do |commit_status| + # We use around_transition to process pipeline on next stages as soon as possible, before the `after_*` is executed + around_transition any => [:success, :failed, :canceled] do |commit_status, block| + block.call + + commit_status.pipeline.try(:process!) + end + + after_transition do |commit_status, transition| + commit_status.pipeline.try(:build_updated) unless transition.loopback? + end + + after_transition [:created, :pending, :running] => :success do |commit_status| MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.pipeline.project, nil).trigger(commit_status) end diff --git a/app/models/concerns/statuseable.rb b/app/models/concerns/statuseable.rb index 44c6b30f2788a89a4425950f6b0d70eaafc6f7b5..5d4b0a868998c82032f261ea6982cf665b9fef1e 100644 --- a/app/models/concerns/statuseable.rb +++ b/app/models/concerns/statuseable.rb @@ -1,18 +1,22 @@ module Statuseable extend ActiveSupport::Concern - AVAILABLE_STATUSES = %w(pending running success failed canceled skipped) + AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped] + STARTED_STATUSES = %w[running success failed skipped] + ACTIVE_STATUSES = %w[pending running] + COMPLETED_STATUSES = %w[success failed canceled] class_methods do def status_sql - builds = all.select('count(*)').to_sql - success = all.success.select('count(*)').to_sql - ignored = all.ignored.select('count(*)').to_sql if all.respond_to?(:ignored) + scope = all.relevant + builds = scope.select('count(*)').to_sql + success = scope.success.select('count(*)').to_sql + ignored = scope.ignored.select('count(*)').to_sql if scope.respond_to?(:ignored) ignored ||= '0' - pending = all.pending.select('count(*)').to_sql - running = all.running.select('count(*)').to_sql - canceled = all.canceled.select('count(*)').to_sql - skipped = all.skipped.select('count(*)').to_sql + pending = scope.pending.select('count(*)').to_sql + running = scope.running.select('count(*)').to_sql + canceled = scope.canceled.select('count(*)').to_sql + skipped = scope.skipped.select('count(*)').to_sql deduce_status = "(CASE WHEN (#{builds})=0 THEN NULL @@ -48,7 +52,8 @@ def finished_at included do validates :status, inclusion: { in: AVAILABLE_STATUSES } - state_machine :status, initial: :pending do + state_machine :status, initial: :created do + state :created, value: 'created' state :pending, value: 'pending' state :running, value: 'running' state :failed, value: 'failed' @@ -57,6 +62,8 @@ def finished_at state :skipped, value: 'skipped' end + scope :created, -> { where(status: 'created') } + scope :relevant, -> { where.not(status: 'created') } scope :running, -> { where(status: 'running') } scope :pending, -> { where(status: 'pending') } scope :success, -> { where(status: 'success') } @@ -68,14 +75,14 @@ def finished_at end def started? - !pending? && !canceled? && started_at + STARTED_STATUSES.include?(status) && started_at end def active? - running? || pending? + ACTIVE_STATUSES.include?(status) end def complete? - canceled? || success? || failed? + COMPLETED_STATUSES.include?(status) end end diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 1dbf2e715038d3a17a532cfb89cb6b7585d69ce7..37f134f9afc8098e3a4b09d4c4af5dd95ae3900c 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -8,6 +8,7 @@ class ProjectMember < Member # Make sure project member points only to project as it source default_value_for :source_type, SOURCE_TYPE validates_format_of :source_type, with: /\AProject\z/ + validates :access_level, inclusion: { in: Gitlab::Access.values } default_scope { where(source_type: SOURCE_TYPE) } scope :in_project, ->(project) { where(source_id: project.id) } diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 64d73d0db97a198ea330b72c60abfd7a99c55357..73f1aac4cd78f9036ae5aa6b66ab11d809dded5a 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -108,6 +108,8 @@ class MergeRequest < ActiveRecord::Base scope :from_project, ->(project) { where(source_project_id: project.id) } scope :merged, -> { with_state(:merged) } scope :closed_and_merged, -> { with_states(:closed, :merged) } + scope :from_source_branches, ->(branches) { where(source_branch: branches) } + scope :join_project, -> { joins(:target_project) } scope :references_project, -> { references(:target_project) } diff --git a/app/models/project.rb b/app/models/project.rb index e78eaac1bff6e571045bc239873134a0ba110c39..a140c44f1a114f5bd1b5d517e284e6996e23ccab 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1106,6 +1106,10 @@ def project_member(user) project_members.find_by(user_id: user) end + def add_user(user, access_level, current_user = nil) + team.add_user(user, access_level, current_user) + end + def default_branch @default_branch ||= repository.root_ref if repository.exists? end diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb index 511b2eac792dbd1e0cdb0b92fc0d3673f9dd9b8d..5af93860d09df945945a21ed4621a6cbb7b97a71 100644 --- a/app/models/project_services/campfire_service.rb +++ b/app/models/project_services/campfire_service.rb @@ -1,4 +1,6 @@ class CampfireService < Service + include HTTParty + prop_accessor :token, :subdomain, :room validates :token, presence: true, if: :activated? @@ -29,18 +31,53 @@ def supported_events def execute(data) return unless supported_events.include?(data[:object_kind]) - room = gate.find_room_by_name(self.room) - return true unless room - + self.class.base_uri base_uri message = build_message(data) - - room.speak(message) + speak(self.room, message, auth) end private - def gate - @gate ||= Tinder::Campfire.new(subdomain, token: token) + def base_uri + @base_uri ||= "https://#{subdomain}.campfirenow.com" + end + + def auth + # use a dummy password, as explained in the Campfire API doc: + # https://github.com/basecamp/campfire-api#authentication + @auth ||= { + basic_auth: { + username: token, + password: 'X' + } + } + end + + # Post a message into a room, returns the message Hash in case of success. + # Returns nil otherwise. + # https://github.com/basecamp/campfire-api/blob/master/sections/messages.md#create-message + def speak(room_name, message, auth) + room = rooms(auth).find { |r| r["name"] == room_name } + return nil unless room + + path = "/room/#{room["id"]}/speak.json" + body = { + body: { + message: { + type: 'TextMessage', + body: message + } + } + } + res = self.class.post(path, auth.merge(body)) + res.code == 201 ? res : nil + end + + # Returns a list of rooms, or []. + # https://github.com/basecamp/campfire-api/blob/master/sections/rooms.md#get-rooms + def rooms(auth) + res = self.class.get("/rooms.json", auth) + res.code == 200 ? res["rooms"] : [] end def build_message(push) diff --git a/app/models/repository.rb b/app/models/repository.rb index 0e2dd9e5d0afae7d84643598031f5b55fe3665ca..9d1e6e9b6a31131c0609bba7ef413005040c5866 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -710,9 +710,7 @@ def branches_sorted_by(value) def tags_sorted_by(value) case value when 'name' - # Would be better to use `sort_by` but `version_sorter` only exposes - # `sort` and `rsort` - VersionSorter.rsort(tag_names).map { |tag_name| find_tag(tag_name) } + VersionSorter.rsort(tags) { |tag| tag.name } when 'updated_desc' tags_sorted_by_committed_date.reverse when 'updated_asc' diff --git a/app/models/spam_report.rb b/app/models/spam_report.rb deleted file mode 100644 index cdc7321b08e7cd5091d1a8cc23b5179f23f79e02..0000000000000000000000000000000000000000 --- a/app/models/spam_report.rb +++ /dev/null @@ -1,5 +0,0 @@ -class SpamReport < ActiveRecord::Base - belongs_to :user - - validates :user, presence: true -end diff --git a/app/models/user.rb b/app/models/user.rb index 7fff4b84e8c8f64c701c1a2c7d30b2dd89a7304e..0169bc559d7e66307dd3f331abb5a699b5eceef9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -23,13 +23,13 @@ class User < ActiveRecord::Base default_value_for :theme_id, gitlab_config.default_theme attr_encrypted :otp_secret, - key: Gitlab::Application.config.secret_key_base, + key: Gitlab::Application.secrets.otp_key_base, mode: :per_attribute_iv_and_salt, insecure_mode: true, algorithm: 'aes-256-cbc' devise :two_factor_authenticatable, - otp_secret_encryption_key: Gitlab::Application.config.secret_key_base + otp_secret_encryption_key: Gitlab::Application.secrets.otp_key_base devise :two_factor_backupable, otp_number_of_backup_codes: 10 serialize :otp_backup_codes, JSON diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb deleted file mode 100644 index 4946f7076fdd515ee1781bccd9bf2af28336ca1d..0000000000000000000000000000000000000000 --- a/app/services/ci/create_builds_service.rb +++ /dev/null @@ -1,62 +0,0 @@ -module Ci - class CreateBuildsService - def initialize(pipeline) - @pipeline = pipeline - @config = pipeline.config_processor - end - - def execute(stage, user, status, trigger_request = nil) - builds_attrs = @config.builds_for_stage_and_ref(stage, @pipeline.ref, @pipeline.tag, trigger_request) - - # check when to create next build - builds_attrs = builds_attrs.select do |build_attrs| - case build_attrs[:when] - when 'on_success' - status == 'success' - when 'on_failure' - status == 'failed' - when 'always', 'manual' - %w(success failed).include?(status) - end - end - - # don't create the same build twice - builds_attrs.reject! do |build_attrs| - @pipeline.builds.find_by(ref: @pipeline.ref, - tag: @pipeline.tag, - trigger_request: trigger_request, - name: build_attrs[:name]) - end - - builds_attrs.map do |build_attrs| - build_attrs.slice!(:name, - :commands, - :tag_list, - :options, - :allow_failure, - :stage, - :stage_idx, - :environment, - :when, - :yaml_variables) - - build_attrs.merge!(pipeline: @pipeline, - ref: @pipeline.ref, - tag: @pipeline.tag, - trigger_request: trigger_request, - user: user, - project: @pipeline.project) - - # TODO: The proper implementation for this is in - # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5295 - build_attrs[:status] = 'skipped' if build_attrs[:when] == 'manual' - - ## - # We do not persist new builds here. - # Those will be persisted when @pipeline is saved. - # - @pipeline.builds.new(build_attrs) - end - end - end -end diff --git a/app/services/ci/create_pipeline_builds_service.rb b/app/services/ci/create_pipeline_builds_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..005014fa1ded2e94b350f06aad0ec2bf1613106f --- /dev/null +++ b/app/services/ci/create_pipeline_builds_service.rb @@ -0,0 +1,42 @@ +module Ci + class CreatePipelineBuildsService < BaseService + attr_reader :pipeline + + def execute(pipeline) + @pipeline = pipeline + + new_builds.map do |build_attributes| + create_build(build_attributes) + end + end + + private + + def create_build(build_attributes) + build_attributes = build_attributes.merge( + pipeline: pipeline, + project: pipeline.project, + ref: pipeline.ref, + tag: pipeline.tag, + user: current_user, + trigger_request: trigger_request + ) + pipeline.builds.create(build_attributes) + end + + def new_builds + @new_builds ||= pipeline.config_builds_attributes. + reject { |build| existing_build_names.include?(build[:name]) } + end + + def existing_build_names + @existing_build_names ||= pipeline.builds.pluck(:name) + end + + def trigger_request + return @trigger_request if defined?(@trigger_request) + + @trigger_request ||= pipeline.trigger_requests.first + end + end +end diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index be91bf0db85a30408b71731674fdf4c02d3fb339..cde856b0186833916b5848f0762e8fdfedf3b725 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -1,49 +1,101 @@ module Ci class CreatePipelineService < BaseService - def execute - pipeline = project.pipelines.new(params) - pipeline.user = current_user + attr_reader :pipeline - unless ref_names.include?(params[:ref]) - pipeline.errors.add(:base, 'Reference not found') - return pipeline + def execute(ignore_skip_ci: false, save_on_errors: true, trigger_request: nil) + @pipeline = Ci::Pipeline.new( + project: project, + ref: ref, + sha: sha, + before_sha: before_sha, + tag: tag?, + trigger_requests: Array(trigger_request), + user: current_user + ) + + unless project.builds_enabled? + return error('Pipeline is disabled') end - if commit - pipeline.sha = commit.id - else - pipeline.errors.add(:base, 'Commit not found') - return pipeline + unless trigger_request || can?(current_user, :create_pipeline, project) + return error('Insufficient permissions to create a new pipeline') end - unless can?(current_user, :create_pipeline, project) - pipeline.errors.add(:base, 'Insufficient permissions to create a new pipeline') - return pipeline + unless branch? || tag? + return error('Reference not found') + end + + unless commit + return error('Commit not found') end unless pipeline.config_processor - pipeline.errors.add(:base, pipeline.yaml_errors || 'Missing .gitlab-ci.yml file') - return pipeline + unless pipeline.ci_yaml_file + return error('Missing .gitlab-ci.yml file') + end + return error(pipeline.yaml_errors, save: save_on_errors) end - pipeline.save! + if !ignore_skip_ci && skip_ci? + pipeline.skip if save_on_errors + return pipeline + end - unless pipeline.create_builds(current_user) - pipeline.errors.add(:base, 'No builds for this pipeline.') + unless pipeline.config_builds_attributes.present? + return error('No builds for this pipeline.') end pipeline.save + pipeline.process! pipeline end private - def ref_names - @ref_names ||= project.repository.ref_names + def skip_ci? + pipeline.git_commit_message =~ /\[(ci skip|skip ci)\]/i if pipeline.git_commit_message end def commit - @commit ||= project.commit(params[:ref]) + @commit ||= project.commit(origin_sha || origin_ref) + end + + def sha + commit.try(:id) + end + + def before_sha + params[:checkout_sha] || params[:before] || Gitlab::Git::BLANK_SHA + end + + def origin_sha + params[:checkout_sha] || params[:after] + end + + def origin_ref + params[:ref] + end + + def branch? + project.repository.ref_exists?(Gitlab::Git::BRANCH_REF_PREFIX + ref) + end + + def tag? + project.repository.ref_exists?(Gitlab::Git::TAG_REF_PREFIX + ref) + end + + def ref + Gitlab::Git.ref_name(origin_ref) + end + + def valid_sha? + origin_sha && origin_sha != Gitlab::Git::BLANK_SHA + end + + def error(message, save: false) + pipeline.errors.add(:base, message) + pipeline.drop if save + pipeline end end end diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb index 1e629cf119aa5120997d1dbfa892b741c38deb1d..6af3c1ca5b130d304bbb7b06f44f3877d931034a 100644 --- a/app/services/ci/create_trigger_request_service.rb +++ b/app/services/ci/create_trigger_request_service.rb @@ -1,20 +1,11 @@ module Ci class CreateTriggerRequestService def execute(project, trigger, ref, variables = nil) - commit = project.commit(ref) - return unless commit + trigger_request = trigger.trigger_requests.create(variables: variables) - # check if ref is tag - tag = project.repository.find_tag(ref).present? - - pipeline = project.pipelines.create(sha: commit.sha, ref: ref, tag: tag) - - trigger_request = trigger.trigger_requests.create!( - variables: variables, - pipeline: pipeline, - ) - - if pipeline.create_builds(nil, trigger_request) + pipeline = Ci::CreatePipelineService.new(project, nil, ref: ref). + execute(ignore_skip_ci: true, trigger_request: trigger_request) + if pipeline.persisted? trigger_request end end diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..6f7610d42ba0ced02417ff428267fb0a6e9d34fc --- /dev/null +++ b/app/services/ci/process_pipeline_service.rb @@ -0,0 +1,77 @@ +module Ci + class ProcessPipelineService < BaseService + attr_reader :pipeline + + def execute(pipeline) + @pipeline = pipeline + + # This method will ensure that our pipeline does have all builds for all stages created + if created_builds.empty? + create_builds! + end + + new_builds = + stage_indexes_of_created_builds.map do |index| + process_stage(index) + end + + # Return a flag if a when builds got enqueued + new_builds.flatten.any? + end + + private + + def create_builds! + Ci::CreatePipelineBuildsService.new(project, current_user).execute(pipeline) + end + + def process_stage(index) + current_status = status_for_prior_stages(index) + + created_builds_in_stage(index).select do |build| + process_build(build, current_status) + end + end + + def process_build(build, current_status) + return false unless Statuseable::COMPLETED_STATUSES.include?(current_status) + + if valid_statuses_for_when(build.when).include?(current_status) + build.enqueue + true + else + build.skip + false + end + end + + def valid_statuses_for_when(value) + case value + when 'on_success' + %w[success] + when 'on_failure' + %w[failed] + when 'always' + %w[success failed] + else + [] + end + end + + def status_for_prior_stages(index) + pipeline.builds.where('stage_idx < ?', index).latest.status || 'success' + end + + def stage_indexes_of_created_builds + created_builds.order(:stage_idx).pluck('distinct stage_idx') + end + + def created_builds_in_stage(index) + created_builds.where(stage_idx: index) + end + + def created_builds + pipeline.builds.created + end + end +end diff --git a/app/services/delete_user_service.rb b/app/services/delete_user_service.rb index ce79287e35a4924f034abeeda22187c558607333..2f237de813c6b3f93b4b3e6b33b71fab139b1c8b 100644 --- a/app/services/delete_user_service.rb +++ b/app/services/delete_user_service.rb @@ -18,7 +18,7 @@ def execute(user, options = {}) user.personal_projects.each do |project| # Skip repository removal because we remove directory with namespace # that contain all this repositories - ::Projects::DestroyService.new(project, current_user, skip_repo: true).pending_delete! + ::Projects::DestroyService.new(project, current_user, skip_repo: true).async_execute end user.destroy diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb index 3c42ac61be4e3e4ff070adcd6d404071d0a79cfe..a4ebccb5606509ec87b72e51c33039e3a03facd6 100644 --- a/app/services/destroy_group_service.rb +++ b/app/services/destroy_group_service.rb @@ -9,7 +9,7 @@ def execute group.projects.each do |project| # Skip repository removal because we remove directory with namespace # that contain all this repositories - ::Projects::DestroyService.new(project, current_user, skip_repo: true).pending_delete! + ::Projects::DestroyService.new(project, current_user, skip_repo: true).async_execute end group.destroy diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 1edee0a6e69d89b93a501b1641abb76275e4c6c3..cb42ae166f6d2c9d9ca61bcbc6b3f7b3ebcc0b61 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -74,8 +74,12 @@ def update_merge_requests SystemHooksService.new.execute_hooks(build_push_data_system_hook.dup, :push_hooks) @project.execute_hooks(build_push_data.dup, :push_hooks) @project.execute_services(build_push_data.dup, :push_hooks) +<<<<<<< HEAD CreateCommitBuildsService.new.execute(@project, current_user, build_push_data, mirror_update: mirror_update) +======= + Ci::CreatePipelineService.new(project, current_user, build_push_data).execute +>>>>>>> ce/8-11-stable ProjectCacheWorker.perform_async(@project.id) end diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index 8b6da4da2d0da8c81a80bc9f11db3bbc4c5d2b4b..e64ed3cbaec214a8bd2a9f52a2cdbf1bafaa6e30 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -11,12 +11,16 @@ def execute SystemHooksService.new.execute_hooks(build_system_push_data.dup, :tag_push_hooks) project.execute_hooks(@push_data.dup, :tag_push_hooks) project.execute_services(@push_data.dup, :tag_push_hooks) +<<<<<<< HEAD CreateCommitBuildsService.new.execute( project, current_user, @push_data, mirror_update: params[:mirror_update] ) +======= + Ci::CreatePipelineService.new(project, current_user, @push_data).execute +>>>>>>> ce/8-11-stable ProjectCacheWorker.perform_async(project.id) true diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb index 15358f80208ea5f024b79c6b1ba78d2e465e486e..9e3f6af628daa49565ac9e56fd8b9d215906b4dd 100644 --- a/app/services/members/destroy_service.rb +++ b/app/services/members/destroy_service.rb @@ -2,8 +2,9 @@ module Members class DestroyService < BaseService attr_accessor :member, :current_user - def initialize(member, user) - @member, @current_user = member, user + def initialize(member, current_user) + @member = member + @current_user = current_user end def execute diff --git a/app/services/merge_requests/get_urls_service.rb b/app/services/merge_requests/get_urls_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..501fd135e16624ed15f9233b3462bd517acea53e --- /dev/null +++ b/app/services/merge_requests/get_urls_service.rb @@ -0,0 +1,52 @@ +module MergeRequests + class GetUrlsService < BaseService + attr_reader :project + + def initialize(project) + @project = project + end + + def execute(changes) + branches = get_branches(changes) + merge_requests_map = opened_merge_requests_from_source_branches(branches) + branches.map do |branch| + existing_merge_request = merge_requests_map[branch] + if existing_merge_request + url_for_existing_merge_request(existing_merge_request) + else + url_for_new_merge_request(branch) + end + end + end + + private + + def opened_merge_requests_from_source_branches(branches) + merge_requests = MergeRequest.from_project(project).opened.from_source_branches(branches) + merge_requests.inject({}) do |hash, mr| + hash[mr.source_branch] = mr + hash + end + end + + def get_branches(changes) + changes_list = Gitlab::ChangesList.new(changes) + changes_list.map do |change| + next unless Gitlab::Git.branch_ref?(change[:ref]) + Gitlab::Git.branch_name(change[:ref]) + end.compact + end + + def url_for_new_merge_request(branch_name) + merge_request_params = { source_branch: branch_name } + url = Gitlab::Routing.url_helpers.new_namespace_project_merge_request_url(project.namespace, project, merge_request: merge_request_params) + { branch_name: branch_name, url: url, new_merge_request: true } + end + + def url_for_existing_merge_request(merge_request) + target_project = merge_request.target_project + url = Gitlab::Routing.url_helpers.namespace_project_merge_request_url(target_project.namespace, target_project, merge_request) + { branch_name: merge_request.source_branch, url: url, new_merge_request: false } + end + end +end diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 4c4ff05f6a26dbccce6992e6db3b9526c4305cd8..e2585760463f662fe3f857fc0349d20b83b59ceb 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -6,8 +6,12 @@ class DestroyError < StandardError; end DELETED_FLAG = '+deleted' - def pending_delete! - project.schedule_delete!(current_user.id, params) + def async_execute + project.transaction do + project.update_attribute(:pending_delete, true) + job_id = ProjectDestroyWorker.perform_async(project.id, current_user.id, params) + Rails.logger.info("User #{current_user.id} scheduled destruction of project #{project.path_with_namespace} with job ID #{job_id}") + end end def execute diff --git a/app/services/projects/enable_deploy_key_service.rb b/app/services/projects/enable_deploy_key_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..3cf4264ce9bf3b747d9acf80f735fa83fcf84235 --- /dev/null +++ b/app/services/projects/enable_deploy_key_service.rb @@ -0,0 +1,17 @@ +module Projects + class EnableDeployKeyService < BaseService + def execute + key = accessible_keys.find_by(id: params[:key_id] || params[:id]) + return unless key + + project.deploy_keys << key + key + end + + private + + def accessible_keys + current_user.accessible_deploy_keys + end + end +end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 9a13af68b2d2119d49d28bf1732703e2bc8dc6cb..b54597468db165e3315f544140f6039b8c77e874 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -389,7 +389,9 @@ .col-sm-10 = f.select :repository_storage, repository_storage_options_for_select, {}, class: 'form-control' .help-block - You can manage the repository storage paths in your gitlab.yml configuration file + Manage repository storage paths. Learn more in the + = succeed "." do + = link_to "repository storages documentation", help_page_path("administration/repository_storages") %fieldset %legend Repository Checks diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml index 448aa95354818b0ac87f39ac546984585a2ff99e..602cfa9b6fc8e7529acf9d9d8f4415956d8891f1 100644 --- a/app/views/admin/labels/_form.html.haml +++ b/app/views/admin/labels/_form.html.haml @@ -28,6 +28,3 @@ .form-actions = f.submit 'Save', class: 'btn btn-save js-save-button' = link_to "Cancel", admin_labels_path, class: 'btn btn-cancel' - -:javascript - new Labels(); diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index deaaf9af8751862c8cb991e7dd1e9a74a0d2e7ef..54ff1d27c67bd1c58057df9ed768aacdb55abe39 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -4,10 +4,6 @@ %i.fa.fa-github Import projects from GitHub -%p - %i.fa.fa-warning - To import GitHub pull requests, any pull request source branches that had been deleted are temporarily restored on GitHub. To prevent any connected CI services from being overloaded with dozens of irrelevant branches being created and deleted again, GitHub webhooks are temporarily disabled during the import process, but only if you have admin access to the GitHub repository. - %p.light Select projects you want to import. %hr diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index ee9c0366f2bdea8131a4f63883492dea0aa60298..9fe94291db749dfa5c8c337213dfa04339678daa 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -6,13 +6,13 @@ - content_for :scripts_body_top do - project = @target_project || @project - if @project_wiki && @page - - markdown_preview_path = namespace_project_wiki_markdown_preview_path(project.namespace, project, @page.slug) + - preview_markdown_path = namespace_project_wiki_preview_markdown_path(project.namespace, project, @page.slug) - else - - markdown_preview_path = markdown_preview_namespace_project_path(project.namespace, project) + - preview_markdown_path = preview_markdown_namespace_project_path(project.namespace, project) - if current_user :javascript window.project_uploads_path = "#{namespace_project_uploads_path project.namespace,project}"; - window.markdown_preview_path = "#{markdown_preview_path}"; + window.preview_markdown_path = "#{preview_markdown_path}"; - content_for :scripts_body do = render "layouts/init_auto_complete" if current_user diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index c854634effc552d15b6df9621929ce6ab81e683f..549925c83c5f6251b881e0fba76a8fa792ee44fa 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -30,6 +30,3 @@ .project-clone-holder = render "shared/clone_panel" - -:javascript - new Star(); diff --git a/app/views/projects/badges/badge.svg.erb b/app/views/projects/badges/badge.svg.erb new file mode 100644 index 0000000000000000000000000000000000000000..a5fef4fc56faf2a2ff341969936dcd214c04853b --- /dev/null +++ b/app/views/projects/badges/badge.svg.erb @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + <%= badge.key_text %> + + + <%= badge.key_text %> + + + <%= badge.value_text %> + + + <%= badge.value_text %> + + + + diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 9a594877803abafa69952fff757c9fcfdace3978..78709a92aedf405fbf7cd787b05a05aa0634fe9d 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -33,7 +33,7 @@ Cant find HEAD commit for this branch - - stages_status = pipeline.statuses.latest.stages_status + - stages_status = pipeline.statuses.relevant.latest.stages_status - stages.each do |stage| %td.stage-cell - status = stages_status[stage] diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index 540689f4a6147717d3c50ec801c8e21eb92cfbbb..640abdb993fa09c441c23f63c7e6a5a37cb1d6ad 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -46,5 +46,5 @@ - if pipeline.project.build_coverage_enabled? %th Coverage %th - - pipeline.statuses.stages.each do |stage| - = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.where(stage: stage) + - pipeline.statuses.relevant.stages.each do |stage| + = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage) diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml index f70dba224faabe12d806c82879f5bf405ddc017f..f7bf3b834ef469da187f4297d8c0a0ae2b54ca14 100644 --- a/app/views/projects/deployments/_actions.haml +++ b/app/views/projects/deployments/_actions.haml @@ -2,9 +2,9 @@ .pull-right - actions = deployment.manual_actions - if actions.present? - .btn-group.inline - .btn-group - %a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} + .inline + .dropdown + %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} = icon("play") %b.caret %ul.dropdown-menu.dropdown-menu-align-right diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml index 0f9d9512d887b122d111ebfcee2ebdffbeadfa23..28813babd7be6b54c0245e5cea545cd2fff47073 100644 --- a/app/views/projects/deployments/_commit.html.haml +++ b/app/views/projects/deployments/_commit.html.haml @@ -1,12 +1,16 @@ %div.branch-commit - if deployment.ref - = link_to deployment.ref, namespace_project_commits_path(@project.namespace, @project, deployment.ref), class: "monospace" - · + .icon-container + = deployment.tag? ? icon('tag') : icon('code-fork') + = link_to deployment.ref, namespace_project_commits_path(@project.namespace, @project, deployment.ref), class: "monospace branch-name" + .icon-container + = custom_icon("icon_commit") = link_to deployment.short_sha, namespace_project_commit_path(@project.namespace, @project, deployment.sha), class: "commit-id monospace" %p.commit-title %span - if commit_title = deployment.commit_title + = author_avatar(deployment.commit, size: 20) = link_to_gfm commit_title, namespace_project_commit_path(@project.namespace, @project, deployment.sha), class: "commit-row-message" - else Cant find HEAD commit for this branch diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml index baf02f1e6a013ca74f5af737d0f86195b6c80f5b..cd95841ca5a9fb23c4d97bec62518d4f9aea9b63 100644 --- a/app/views/projects/deployments/_deployment.html.haml +++ b/app/views/projects/deployments/_deployment.html.haml @@ -8,6 +8,7 @@ %td - if deployment.deployable = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable] do + = user_avatar(user: deployment.user, size: 20) = "#{deployment.deployable.name} (##{deployment.deployable.id})" %td diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index f0a86fd6d40e054c49d45137172c422c58cdbb41..8fbd89100ca5e23c1bd42d4dde9f5f92e5957972 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -1,4 +1,4 @@ -.diff-file.file-holder{id: "diff-#{index}", data: diff_file_html_data(project, diff_file)} +.diff-file.file-holder{id: "diff-#{index}", data: diff_file_html_data(project, diff_file.file_path, diff_commit.id)} .file-title{id: "file-path-#{hexdigest(diff_file.file_path)}"} = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "#diff-#{index}" diff --git a/app/views/projects/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml index e2453395602c1f6fe82a61a73c2268e491c3942f..36a6162a5a862e33b77518f17908b20c90253190 100644 --- a/app/views/projects/environments/_environment.html.haml +++ b/app/views/projects/environments/_environment.html.haml @@ -2,8 +2,12 @@ %tr.environment %td - %strong - = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment) + = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment) + + %td + - if last_deployment + = user_avatar(user: last_deployment.user, size: 20) + %strong ##{last_deployment.id} %td - if last_deployment diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index a6dd34653abd84e4b0db61e0ae5aa38388ae7364..b3eb5b0011a9617efc1b7252764c89bd429e955f 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -23,10 +23,11 @@ New environment - else .table-holder - %table.table.environments + %table.table.builds.environments %tbody %th Environment - %th Last deployment - %th Date + %th Last Deployment + %th Commit + %th %th = render @environments diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index a07436ad7c97dd5f3b246644f5c3680a0cd8d852..8f8c1c4ce22c09b11b9ad4fde9b7ec5a91d28042 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -23,13 +23,13 @@ = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success" - else .table-holder - %table.table.environments + %table.table.builds.environments %thead %tr %th ID %th Commit %th Build - %th Date + %th %th = render @deployments diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index facdfcc9447f71fc0970833eadd6e49a63cb9063..adcc984f5067ce1296123c28903594b4aafe2426 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -46,28 +46,18 @@ %div - if github_import_enabled? = link_to new_import_github_path, class: 'btn import_github' do - = icon 'github', text: 'GitHub' + = icon('github', text: 'GitHub') %div - if bitbucket_import_enabled? - - if bitbucket_import_configured? - = link_to status_import_bitbucket_path, class: 'btn import_bitbucket', "data-no-turbolink" => "true" do - %i.fa.fa-bitbucket - Bitbucket - - else - = link_to status_import_bitbucket_path, class: 'how_to_import_link btn import_bitbucket', "data-no-turbolink" => "true" do - %i.fa.fa-bitbucket - Bitbucket + = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}", "data-no-turbolink" => "true" do + = icon('bitbucket', text: 'Bitbucket') + - unless bitbucket_import_configured? = render 'bitbucket_import_modal' %div - if gitlab_import_enabled? - - if gitlab_import_configured? - = link_to status_import_gitlab_path, class: 'btn import_gitlab' do - %i.fa.fa-heart - GitLab.com - - else - = link_to status_import_gitlab_path, class: 'how_to_import_link btn import_gitlab' do - %i.fa.fa-heart - GitLab.com + = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless bitbucket_import_configured?}" do + = icon('gitlab', text: 'GitLab.com') + - unless gitlab_import_configured? = render 'gitlab_import_modal' %div - if gitorious_import_enabled? @@ -77,23 +67,19 @@ %div - if google_code_import_enabled? = link_to new_import_google_code_path, class: 'btn import_google_code' do - %i.fa.fa-google - Google Code + = icon('google', text: 'Google Code') %div - if fogbugz_import_enabled? = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do - %i.fa.fa-bug - Fogbugz + = icon('bug', text: 'Fogbugz') %div - if git_import_enabled? = link_to "#", class: 'btn js-toggle-button import_git' do - %i.fa.fa-git - %span Repo by URL + = icon('git', text: 'Repo by URL') %div{ class: 'import_gitlab_project' } - if gitlab_project_import_enabled? = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do - %i.fa.fa-gitlab - %span GitLab export + = icon('gitlab', text: 'GitLab export') .js-toggle-content.hide = render "shared/import_form", f: f @@ -159,4 +145,4 @@ $('.import_git').click(function( event ) { $projectImportUrl = $('#project_import_url') $projectImportUrl.attr('disabled', !$projectImportUrl.attr('disabled')) - }); \ No newline at end of file + }); diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml index 5f4ec2e40c85e9c510aeebe395a735f4960007cb..55202725b9ee74c808a4b96148fd5365b93a1d8b 100644 --- a/app/views/projects/pipelines/new.html.haml +++ b/app/views/projects/pipelines/new.html.haml @@ -9,7 +9,7 @@ .form-group = f.label :ref, 'Create for', class: 'control-label' .col-sm-10 - = f.text_field :ref, required: true, tabindex: 2, class: 'form-control' + = f.text_field :ref, required: true, tabindex: 2, class: 'form-control js-branch-name ui-autocomplete-input', autocomplete: :false, id: :ref .help-block Existing branch name, tag .form-actions = f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3 diff --git a/app/views/projects/tree/_tree_row.html.haml b/app/views/projects/tree/_tree_row.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..0a5c6f048f72026346c143ce189915c3c63ea0dc --- /dev/null +++ b/app/views/projects/tree/_tree_row.html.haml @@ -0,0 +1,6 @@ +- if tree_row.type == :tree + = render partial: 'projects/tree/tree_item', object: tree_row, as: 'tree_item', locals: { type: 'folder' } +- elsif tree_row.type == :blob + = render partial: 'projects/tree/blob_item', object: tree_row, as: 'blob_item', locals: { type: 'file' } +- elsif tree_row.type == :commit + = render partial: 'projects/tree/submodule_item', object: tree_row, as: 'submodule_item' diff --git a/app/views/shared/_labels_row.html.haml b/app/views/shared/_labels_row.html.haml index dce492352ac6189543ed450268a6a39c507177df..e324d0e5203e4a1dbe51d6438b97fc46f9cad6e3 100644 --- a/app/views/shared/_labels_row.html.haml +++ b/app/views/shared/_labels_row.html.haml @@ -1,9 +1,5 @@ - labels.each do |label| %span.label-row.btn-group{ role: "group", aria: { label: label.name }, style: "color: #{text_color_for_bg(label.color)}" } - = link_to label.name, label_filter_path(@project, label, type: controller.controller_name), - class: "btn btn-transparent has-tooltip", - style: "background-color: #{label.color};", - title: escape_once(label.description), - data: { container: "body" } + = link_to_label(label, css_class: 'btn btn-transparent') %button.btn.btn-transparent.label-remove.js-label-filter-remove{ type: "button", style: "background-color: #{label.color};", data: { label: label.title } } = icon("times") diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 5ae485f36baa08ff4c7913e4c8ba1303178960ad..fc6e206d0821c01ddedc088c0669601cc29d2a34 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -1,4 +1,4 @@ -- show_roles = local_assigns.fetch(:show_roles, default_show_roles(member)) +- show_roles = local_assigns.fetch(:show_roles, true) - show_controls = local_assigns.fetch(:show_controls, true) - user = member.user diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index b8b66d08db8585e5c2a850f7ca3106af8e596ead..92803838d02ea8c91ec30ed7acc142fb6a024438 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -24,7 +24,7 @@ = icon('star') = project.star_count %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project)} - = visibility_level_icon(project.visibility_level, fw: false) + = visibility_level_icon(project.visibility_level, fw: true) .title = link_to project_path(project), class: dom_class(project) do diff --git a/config/application.rb b/config/application.rb index ee27f16055b33e55a60d86f769a9068a7df94be4..7b1833888e5e5afc65cecfd91b85886f3bd757a4 100644 --- a/config/application.rb +++ b/config/application.rb @@ -110,7 +110,8 @@ class Application < Rails::Application end end - redis_config_hash = Gitlab::Redis.redis_store_options + # Use Redis caching across all environments + redis_config_hash = Gitlab::Redis.params redis_config_hash[:namespace] = Gitlab::Redis::CACHE_NAMESPACE redis_config_hash[:expires_in] = 2.weeks # Cache should not grow forever config.cache_store = :redis_store, redis_config_hash diff --git a/config/initializers/5_backend.rb b/config/initializers/5_backend.rb index e026151a03270c1502fc3ce68f4abbcf518c3aec..ed88c8ee1b827d043d1cb2a815112a3280a5a982 100644 --- a/config/initializers/5_backend.rb +++ b/config/initializers/5_backend.rb @@ -1,6 +1,3 @@ -# GIT over HTTP -require_dependency Rails.root.join('lib/gitlab/backend/grack_auth') - # GIT over SSH require_dependency Rails.root.join('lib/gitlab/backend/shell') diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index f74dd1d556811fc9c5f50407c246c30f0afe6e89..9f7e441a9f270d22e13f275961b936878c42dd35 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -100,6 +100,9 @@ # secure: true in order to force SSL only cookies. # config.cookie_options = {} + # Send a notification email when the user's password is changed + config.send_password_change_notification = true + # ==> Configuration for :validatable # Range for password length. Default is 6..128. config.password_length = 8..128 diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb index a0d5ddf2aad02de9ae61c887fef80cffda032a9c..5b369e7510396cf9cdcf8f9284e02476f92c0468 100644 --- a/config/initializers/metrics.rb +++ b/config/initializers/metrics.rb @@ -149,6 +149,9 @@ config.instrument_methods(Gitlab::Highlight) config.instrument_instance_methods(Gitlab::Highlight) + # This is a Rails scope so we have to instrument it manually. + config.instrument_method(Project, :visible_to_user) + config.instrument_instance_methods(Search::GlobalService) config.instrument_instance_methods(Search::ProjectService) config.instrument_instance_methods(Gitlab::Elastic::SearchResults) diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index 3e55312020593edad806eb627e1b9e749937d7c1..f498732feca21222c6c0dc921b3482463a35a7f0 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -12,3 +12,10 @@ Mime::Type.register "video/mp4", :mp4, [], [:m4v, :mov] Mime::Type.register "video/webm", :webm Mime::Type.register "video/ogg", :ogv + +middlewares = Gitlab::Application.config.middleware +middlewares.swap(ActionDispatch::ParamsParser, ActionDispatch::ParamsParser, { + Mime::Type.lookup('application/vnd.git-lfs+json') => lambda do |body| + ActiveSupport::JSON.decode(body) + end +}) diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb index dae3a4a9a93f26da1b6c9289bf806639508b38c3..291fa6c0abcfd0f3361d585642c2803373aa3f34 100644 --- a/config/initializers/secret_token.rb +++ b/config/initializers/secret_token.rb @@ -2,49 +2,86 @@ require 'securerandom' -# Your secret key for verifying the integrity of signed cookies. -# If you change this key, all old signed cookies will become invalid! -# Make sure the secret is at least 30 characters and all random, -# no regular words or you'll be exposed to dictionary attacks. - -def find_secure_token - token_file = Rails.root.join('.secret') - if ENV.key?('SECRET_KEY_BASE') - ENV['SECRET_KEY_BASE'] - elsif File.exist? token_file - # Use the existing token. - File.read(token_file).chomp - else - # Generate a new token of 64 random hexadecimal characters and store it in token_file. - token = SecureRandom.hex(64) - File.write(token_file, token) - token +# Transition material in .secret to the secret_key_base key in config/secrets.yml. +# Historically, ENV['SECRET_KEY_BASE'] takes precedence over .secret, so we maintain that +# behavior. +# +# It also used to be the case that the key material in ENV['SECRET_KEY_BASE'] or .secret +# was used to encrypt OTP (two-factor authentication) data so if present, we copy that key +# material into config/secrets.yml under otp_key_base. +# +# Finally, if we have successfully migrated all secrets to config/secrets.yml, delete the +# .secret file to avoid confusion. +# +def create_tokens + secret_file = Rails.root.join('.secret') + file_secret_key = File.read(secret_file).chomp if File.exist?(secret_file) + env_secret_key = ENV['SECRET_KEY_BASE'] + + # Ensure environment variable always overrides secrets.yml. + Rails.application.secrets.secret_key_base = env_secret_key if env_secret_key.present? + + defaults = { + secret_key_base: file_secret_key || generate_new_secure_token, + otp_key_base: env_secret_key || file_secret_key || generate_new_secure_token, + db_key_base: generate_new_secure_token + } + + missing_secrets = set_missing_keys(defaults) + write_secrets_yml(missing_secrets) unless missing_secrets.empty? + + begin + File.delete(secret_file) if file_secret_key + rescue => e + warn "Error deleting useless .secret file: #{e}" end end -Rails.application.config.secret_token = find_secure_token -Rails.application.config.secret_key_base = find_secure_token - -# CI def generate_new_secure_token SecureRandom.hex(64) end -if Rails.application.secrets.db_key_base.blank? - warn "Missing `db_key_base` for '#{Rails.env}' environment. The secrets will be generated and stored in `config/secrets.yml`" +def warn_missing_secret(secret) + warn "Missing Rails.application.secrets.#{secret} for #{Rails.env} environment. The secret will be generated and stored in config/secrets.yml." +end - all_secrets = YAML.load_file('config/secrets.yml') if File.exist?('config/secrets.yml') - all_secrets ||= {} +def set_missing_keys(defaults) + defaults.stringify_keys.each_with_object({}) do |(key, default), missing| + if Rails.application.secrets[key].blank? + warn_missing_secret(key) - # generate secrets - env_secrets = all_secrets[Rails.env.to_s] || {} - env_secrets['db_key_base'] ||= generate_new_secure_token - all_secrets[Rails.env.to_s] = env_secrets + missing[key] = Rails.application.secrets[key] = default + end + end +end + +def write_secrets_yml(missing_secrets) + secrets_yml = Rails.root.join('config/secrets.yml') + rails_env = Rails.env.to_s + secrets = YAML.load_file(secrets_yml) if File.exist?(secrets_yml) + secrets ||= {} + secrets[rails_env] ||= {} + + secrets[rails_env].merge!(missing_secrets) do |key, old, new| + # Previously, it was possible this was set to the literal contents of an Erb + # expression that evaluated to an empty value. We don't want to support that + # specifically, just ensure we don't break things further. + # + if old.present? + warn < + if Gitlab::MailRoom.enabled? + %> - - :host: <%= config['host'].to_json %> - :port: <%= config['port'].to_json %> - :ssl: <%= config['ssl'].to_json %> - :start_tls: <%= config['start_tls'].to_json %> - :email: <%= config['user'].to_json %> - :password: <%= config['password'].to_json %> + :host: <%= config[:host].to_json %> + :port: <%= config[:port].to_json %> + :ssl: <%= config[:ssl].to_json %> + :start_tls: <%= config[:start_tls].to_json %> + :email: <%= config[:user].to_json %> + :password: <%= config[:password].to_json %> + :idle_timeout: 60 - :name: <%= config['mailbox'].to_json %> + :name: <%= config[:mailbox].to_json %> :delete_after_delivery: true :delivery_method: sidekiq :delivery_options: - :redis_url: <%= redis_url.to_json %> - :namespace: resque:gitlab + :redis_url: <%= config[:redis_url].to_json %> + :namespace: <%= Gitlab::Redis::SIDEKIQ_NAMESPACE %> :queue: incoming_email :worker: EmailReceiverWorker :arbitration_method: redis :arbitration_options: - :redis_url: <%= redis_url.to_json %> - :namespace: mail_room:gitlab + :redis_url: <%= config[:redis_url].to_json %> + :namespace: <%= Gitlab::Redis::MAILROOM_NAMESPACE %> + <% end %> -<% end %> diff --git a/config/resque.yml.example b/config/resque.yml.example index d98f43f71b298743b7feeef51e64cde6e73802fd..0c19d8bc1d36afd58a8e6964ce0fc7293c5d0613 100644 --- a/config/resque.yml.example +++ b/config/resque.yml.example @@ -1,6 +1,34 @@ # If you change this file in a Merge Request, please also create # a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests # -development: redis://localhost:6379 -test: redis://localhost:6379 -production: unix:/var/run/redis/redis.sock +development: + url: redis://localhost:6379 + # sentinels: + # - + # host: localhost + # port: 26380 # point to sentinel, not to redis port + # - + # host: slave2 + # port: 26381 # point to sentinel, not to redis port +test: + url: redis://localhost:6379 +production: + # Redis (single instance) + url: unix:/var/run/redis/redis.sock + ## + # Redis + Sentinel (for HA) + # + # Please read instructions carefully before using it as you may lose data: + # http://redis.io/topics/sentinel + # + # You must specify a list of a few sentinels that will handle client connection + # please read here for more information: https://docs.gitlab.com/ce/administration/high_availability/redis.html + ## + # url: redis://master:6379 + # sentinels: + # - + # host: slave1 + # port: 26379 # point to sentinel, not to redis port + # - + # host: slave2 + # port: 26379 # point to sentinel, not to redis port diff --git a/config/routes.rb b/config/routes.rb index fd8c6b6301956e299888e5a5fed9bc7209a51d63..0eb6bd5bbdfd754302f0f614929659330df8aae7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -92,9 +92,6 @@ # Health check get 'health_check(/:checks)' => 'health_check#index', as: :health_check - # Enable Grack support (for LFS only) - mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\/(info\/lfs|gitlab-lfs)/.match(request.path_info) }, via: [:get, :post, :put] - # Help get 'help' => 'help#index' get 'help/shortcuts' => 'help#shortcuts' @@ -515,7 +512,7 @@ post :unarchive post :housekeeping post :toggle_star - post :markdown_preview + post :preview_markdown post :export post :remove_export post :generate_new_export @@ -526,11 +523,26 @@ end scope module: :projects do - # Git HTTP clients ('git clone' etc.) scope constraints: { id: /.+\.git/, format: nil } do + # Git HTTP clients ('git clone' etc.) get '/info/refs', to: 'git_http#info_refs' post '/git-upload-pack', to: 'git_http#git_upload_pack' post '/git-receive-pack', to: 'git_http#git_receive_pack' + + # Git LFS API (metadata) + post '/info/lfs/objects/batch', to: 'lfs_api#batch' + post '/info/lfs/objects', to: 'lfs_api#deprecated' + get '/info/lfs/objects/*oid', to: 'lfs_api#deprecated' + + # GitLab LFS object storage + scope constraints: { oid: /[a-f0-9]{64}/ } do + get '/gitlab-lfs/objects/*oid', to: 'lfs_storage#download' + + scope constraints: { size: /[0-9]+/ } do + put '/gitlab-lfs/objects/*oid/*size/authorize', to: 'lfs_storage#upload_authorize' + put '/gitlab-lfs/objects/*oid/*size', to: 'lfs_storage#upload_finalize' + end + end end # Allow /info/refs, /info/refs?service=git-upload-pack, and @@ -708,7 +720,7 @@ get '/wikis/*id', to: 'wikis#show', as: 'wiki', constraints: WIKI_SLUG_ID delete '/wikis/*id', to: 'wikis#destroy', constraints: WIKI_SLUG_ID put '/wikis/*id', to: 'wikis#update', constraints: WIKI_SLUG_ID - post '/wikis/*id/markdown_preview', to: 'wikis#markdown_preview', constraints: WIKI_SLUG_ID, as: 'wiki_markdown_preview' + post '/wikis/*id/preview_markdown', to: 'wikis#preview_markdown', constraints: WIKI_SLUG_ID, as: 'wiki_preview_markdown' end resource :repository, only: [:create] do diff --git a/db/migrate/20160716115711_add_queued_at_to_ci_builds.rb b/db/migrate/20160716115711_add_queued_at_to_ci_builds.rb new file mode 100644 index 0000000000000000000000000000000000000000..756910a1fa0cdb819d0013d38ba41f1e82dad1d2 --- /dev/null +++ b/db/migrate/20160716115711_add_queued_at_to_ci_builds.rb @@ -0,0 +1,9 @@ +class AddQueuedAtToCiBuilds < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :ci_builds, :queued_at, :timestamp + end +end diff --git a/db/migrate/20160810102349_remove_ci_runner_trigram_indexes.rb b/db/migrate/20160810102349_remove_ci_runner_trigram_indexes.rb new file mode 100644 index 0000000000000000000000000000000000000000..0cfb637804bb56e301245ada56042d5dfd0bad17 --- /dev/null +++ b/db/migrate/20160810102349_remove_ci_runner_trigram_indexes.rb @@ -0,0 +1,27 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveCiRunnerTrigramIndexes < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + # Disabled for the "down" method so the indexes can be re-created concurrently. + disable_ddl_transaction! + + def up + return unless Gitlab::Database.postgresql? + + transaction do + execute 'DROP INDEX IF EXISTS index_ci_runners_on_token_trigram;' + execute 'DROP INDEX IF EXISTS index_ci_runners_on_description_trigram;' + end + end + + def down + return unless Gitlab::Database.postgresql? + + execute 'CREATE INDEX CONCURRENTLY index_ci_runners_on_token_trigram ON ci_runners USING gin(token gin_trgm_ops);' + execute 'CREATE INDEX CONCURRENTLY index_ci_runners_on_description_trigram ON ci_runners USING gin(description gin_trgm_ops);' + end +end diff --git a/db/migrate/20160810142633_remove_redundant_indexes.rb b/db/migrate/20160810142633_remove_redundant_indexes.rb new file mode 100644 index 0000000000000000000000000000000000000000..8641c6ffa8f9fdb5ecf91068bdf3a20e24e3db23 --- /dev/null +++ b/db/migrate/20160810142633_remove_redundant_indexes.rb @@ -0,0 +1,112 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveRedundantIndexes < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + indexes = [ + [:ci_taggings, 'ci_taggings_idx'], + [:audit_events, 'index_audit_events_on_author_id'], + [:audit_events, 'index_audit_events_on_type'], + [:ci_builds, 'index_ci_builds_on_erased_by_id'], + [:ci_builds, 'index_ci_builds_on_project_id_and_commit_id'], + [:ci_builds, 'index_ci_builds_on_type'], + [:ci_commits, 'index_ci_commits_on_project_id'], + [:ci_commits, 'index_ci_commits_on_project_id_and_committed_at'], + [:ci_commits, 'index_ci_commits_on_project_id_and_committed_at_and_id'], + [:ci_commits, 'index_ci_commits_on_project_id_and_sha'], + [:ci_commits, 'index_ci_commits_on_sha'], + [:ci_events, 'index_ci_events_on_created_at'], + [:ci_events, 'index_ci_events_on_is_admin'], + [:ci_events, 'index_ci_events_on_project_id'], + [:ci_jobs, 'index_ci_jobs_on_deleted_at'], + [:ci_jobs, 'index_ci_jobs_on_project_id'], + [:ci_projects, 'index_ci_projects_on_gitlab_id'], + [:ci_projects, 'index_ci_projects_on_shared_runners_enabled'], + [:ci_services, 'index_ci_services_on_project_id'], + [:ci_sessions, 'index_ci_sessions_on_session_id'], + [:ci_sessions, 'index_ci_sessions_on_updated_at'], + [:ci_tags, 'index_ci_tags_on_name'], + [:ci_triggers, 'index_ci_triggers_on_deleted_at'], + [:identities, 'index_identities_on_created_at_and_id'], + [:issues, 'index_issues_on_title'], + [:keys, 'index_keys_on_created_at_and_id'], + [:members, 'index_members_on_created_at_and_id'], + [:members, 'index_members_on_type'], + [:milestones, 'index_milestones_on_created_at_and_id'], + [:namespaces, 'index_namespaces_on_visibility_level'], + [:projects, 'index_projects_on_builds_enabled_and_shared_runners_enabled'], + [:services, 'index_services_on_category'], + [:services, 'index_services_on_created_at_and_id'], + [:services, 'index_services_on_default'], + [:snippets, 'index_snippets_on_created_at'], + [:snippets, 'index_snippets_on_created_at_and_id'], + [:todos, 'index_todos_on_state'], + [:web_hooks, 'index_web_hooks_on_created_at_and_id'], + + # These indexes _may_ be used but they can be replaced by other existing + # indexes. + + # There's already a composite index on (project_id, iid) which means that + # a separate index for _just_ project_id is not needed. + [:issues, 'index_issues_on_project_id'], + + # These are all composite indexes for the columns (created_at, id). In all + # these cases there's already a standalone index for "created_at" which + # can be used instead. + # + # Because the "id" column of these composite indexes is never needed (due + # to "id" already being indexed as its a primary key) these composite + # indexes are useless. + [:issues, 'index_issues_on_created_at_and_id'], + [:merge_requests, 'index_merge_requests_on_created_at_and_id'], + [:namespaces, 'index_namespaces_on_created_at_and_id'], + [:notes, 'index_notes_on_created_at_and_id'], + [:projects, 'index_projects_on_created_at_and_id'], + [:users, 'index_users_on_created_at_and_id'], + ] + + transaction do + indexes.each do |(table, index)| + remove_index(table, name: index) if index_exists_by_name?(table, index) + end + end + + add_concurrent_index(:users, :created_at) + add_concurrent_index(:projects, :created_at) + add_concurrent_index(:namespaces, :created_at) + end + + def down + # We're only restoring the composite indexes that could be replaced with + # individual ones, just in case somebody would ever want to revert. + transaction do + remove_index(:users, :created_at) + remove_index(:projects, :created_at) + remove_index(:namespaces, :created_at) + end + + [:issues, :merge_requests, :namespaces, :notes, :projects, :users].each do |table| + add_concurrent_index(table, [:created_at, :id], + name: "index_#{table}_on_created_at_and_id") + end + end + + # Rails' index_exists? doesn't work when you only give it a table and index + # name. As such we have to use some extra code to check if an index exists for + # a given name. + def index_exists_by_name?(table, index) + indexes_for_table[table].include?(index) + end + + def indexes_for_table + @indexes_for_table ||= Hash.new do |hash, table_name| + hash[table_name] = indexes(table_name).map(&:name) + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 2d6210964551c8f2426359d0ef7d5815e2b895fd..f258559c0cfc1c5b090f7f16ec555822f1af9907 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -128,9 +128,7 @@ t.datetime "updated_at" end - add_index "audit_events", ["author_id"], name: "index_audit_events_on_author_id", using: :btree add_index "audit_events", ["entity_id", "entity_type"], name: "index_audit_events_on_entity_id_and_entity_type", using: :btree - add_index "audit_events", ["type"], name: "index_audit_events_on_type", using: :btree create_table "award_emoji", force: :cascade do |t| t.string "name" @@ -198,6 +196,7 @@ t.integer "artifacts_size" t.string "when" t.text "yaml_variables" + t.datetime "queued_at" end add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree @@ -205,13 +204,10 @@ add_index "ci_builds", ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree add_index "ci_builds", ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree add_index "ci_builds", ["commit_id"], name: "index_ci_builds_on_commit_id", using: :btree - add_index "ci_builds", ["erased_by_id"], name: "index_ci_builds_on_erased_by_id", using: :btree add_index "ci_builds", ["gl_project_id"], name: "index_ci_builds_on_gl_project_id", using: :btree - add_index "ci_builds", ["project_id", "commit_id"], name: "index_ci_builds_on_project_id_and_commit_id", using: :btree add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree add_index "ci_builds", ["status"], name: "index_ci_builds_on_status", using: :btree - add_index "ci_builds", ["type"], name: "index_ci_builds_on_type", using: :btree create_table "ci_commits", force: :cascade do |t| t.integer "project_id" @@ -235,11 +231,6 @@ add_index "ci_commits", ["gl_project_id", "sha"], name: "index_ci_commits_on_gl_project_id_and_sha", using: :btree add_index "ci_commits", ["gl_project_id", "status"], name: "index_ci_commits_on_gl_project_id_and_status", using: :btree add_index "ci_commits", ["gl_project_id"], name: "index_ci_commits_on_gl_project_id", using: :btree - add_index "ci_commits", ["project_id", "committed_at", "id"], name: "index_ci_commits_on_project_id_and_committed_at_and_id", using: :btree - add_index "ci_commits", ["project_id", "committed_at"], name: "index_ci_commits_on_project_id_and_committed_at", using: :btree - add_index "ci_commits", ["project_id", "sha"], name: "index_ci_commits_on_project_id_and_sha", using: :btree - add_index "ci_commits", ["project_id"], name: "index_ci_commits_on_project_id", using: :btree - add_index "ci_commits", ["sha"], name: "index_ci_commits_on_sha", using: :btree add_index "ci_commits", ["status"], name: "index_ci_commits_on_status", using: :btree add_index "ci_commits", ["user_id"], name: "index_ci_commits_on_user_id", using: :btree @@ -252,10 +243,6 @@ t.datetime "updated_at" end - add_index "ci_events", ["created_at"], name: "index_ci_events_on_created_at", using: :btree - add_index "ci_events", ["is_admin"], name: "index_ci_events_on_is_admin", using: :btree - add_index "ci_events", ["project_id"], name: "index_ci_events_on_project_id", using: :btree - create_table "ci_jobs", force: :cascade do |t| t.integer "project_id", null: false t.text "commands" @@ -270,9 +257,6 @@ t.datetime "deleted_at" end - add_index "ci_jobs", ["deleted_at"], name: "index_ci_jobs_on_deleted_at", using: :btree - add_index "ci_jobs", ["project_id"], name: "index_ci_jobs_on_project_id", using: :btree - create_table "ci_projects", force: :cascade do |t| t.string "name" t.integer "timeout", default: 3600, null: false @@ -296,9 +280,6 @@ t.text "generated_yaml_config" end - add_index "ci_projects", ["gitlab_id"], name: "index_ci_projects_on_gitlab_id", using: :btree - add_index "ci_projects", ["shared_runners_enabled"], name: "index_ci_projects_on_shared_runners_enabled", using: :btree - create_table "ci_runner_projects", force: :cascade do |t| t.integer "runner_id", null: false t.integer "project_id" @@ -327,10 +308,8 @@ t.boolean "locked", default: false, null: false end - add_index "ci_runners", ["description"], name: "index_ci_runners_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree - add_index "ci_runners", ["token"], name: "index_ci_runners_on_token_trigram", using: :gin, opclasses: {"token"=>"gin_trgm_ops"} create_table "ci_services", force: :cascade do |t| t.string "type" @@ -342,8 +321,6 @@ t.text "properties" end - add_index "ci_services", ["project_id"], name: "index_ci_services_on_project_id", using: :btree - create_table "ci_sessions", force: :cascade do |t| t.string "session_id", null: false t.text "data" @@ -351,9 +328,6 @@ t.datetime "updated_at" end - add_index "ci_sessions", ["session_id"], name: "index_ci_sessions_on_session_id", using: :btree - add_index "ci_sessions", ["updated_at"], name: "index_ci_sessions_on_updated_at", using: :btree - create_table "ci_taggings", force: :cascade do |t| t.integer "tag_id" t.integer "taggable_id" @@ -364,7 +338,6 @@ t.datetime "created_at" end - add_index "ci_taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "ci_taggings_idx", unique: true, using: :btree add_index "ci_taggings", ["taggable_id", "taggable_type", "context"], name: "index_ci_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree create_table "ci_tags", force: :cascade do |t| @@ -372,8 +345,6 @@ t.integer "taggings_count", default: 0 end - add_index "ci_tags", ["name"], name: "index_ci_tags_on_name", unique: true, using: :btree - create_table "ci_trigger_requests", force: :cascade do |t| t.integer "trigger_id", null: false t.text "variables" @@ -391,7 +362,6 @@ t.integer "gl_project_id" end - add_index "ci_triggers", ["deleted_at"], name: "index_ci_triggers_on_deleted_at", using: :btree add_index "ci_triggers", ["gl_project_id"], name: "index_ci_triggers_on_gl_project_id", using: :btree create_table "ci_variables", force: :cascade do |t| @@ -519,7 +489,6 @@ t.string "secondary_extern_uid" end - add_index "identities", ["created_at", "id"], name: "index_identities_on_created_at_and_id", using: :btree add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree create_table "index_statuses", force: :cascade do |t| @@ -557,16 +526,13 @@ add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree add_index "issues", ["confidential"], name: "index_issues_on_confidential", using: :btree - add_index "issues", ["created_at", "id"], name: "index_issues_on_created_at_and_id", using: :btree add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree add_index "issues", ["deleted_at"], name: "index_issues_on_deleted_at", using: :btree add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "issues", ["due_date"], name: "index_issues_on_due_date", using: :btree add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree - add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree add_index "issues", ["state"], name: "index_issues_on_state", using: :btree - add_index "issues", ["title"], name: "index_issues_on_title", using: :btree add_index "issues", ["title"], name: "index_issues_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"} create_table "keys", force: :cascade do |t| @@ -580,7 +546,6 @@ t.boolean "public", default: false, null: false end - add_index "keys", ["created_at", "id"], name: "index_keys_on_created_at_and_id", using: :btree add_index "keys", ["fingerprint"], name: "index_keys_on_fingerprint", unique: true, using: :btree add_index "keys", ["user_id"], name: "index_keys_on_user_id", using: :btree @@ -660,11 +625,9 @@ end add_index "members", ["access_level"], name: "index_members_on_access_level", using: :btree - add_index "members", ["created_at", "id"], name: "index_members_on_created_at_and_id", using: :btree add_index "members", ["invite_token"], name: "index_members_on_invite_token", unique: true, using: :btree add_index "members", ["requested_at"], name: "index_members_on_requested_at", using: :btree add_index "members", ["source_id", "source_type"], name: "index_members_on_source_id_and_source_type", using: :btree - add_index "members", ["type"], name: "index_members_on_type", using: :btree add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree create_table "merge_request_diffs", force: :cascade do |t| @@ -713,7 +676,6 @@ add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree add_index "merge_requests", ["author_id"], name: "index_merge_requests_on_author_id", using: :btree - add_index "merge_requests", ["created_at", "id"], name: "index_merge_requests_on_created_at_and_id", using: :btree add_index "merge_requests", ["created_at"], name: "index_merge_requests_on_created_at", using: :btree add_index "merge_requests", ["deleted_at"], name: "index_merge_requests_on_deleted_at", using: :btree add_index "merge_requests", ["description"], name: "index_merge_requests_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} @@ -736,7 +698,6 @@ t.integer "iid" end - add_index "milestones", ["created_at", "id"], name: "index_milestones_on_created_at_and_id", using: :btree add_index "milestones", ["description"], name: "index_milestones_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree add_index "milestones", ["project_id", "iid"], name: "index_milestones_on_project_id_and_iid", unique: true, using: :btree @@ -767,13 +728,13 @@ add_index "namespaces", ["created_at", "id"], name: "index_namespaces_on_created_at_and_id", using: :btree add_index "namespaces", ["ldap_sync_last_successful_update_at"], name: "index_namespaces_on_ldap_sync_last_successful_update_at", using: :btree add_index "namespaces", ["ldap_sync_last_update_at"], name: "index_namespaces_on_ldap_sync_last_update_at", using: :btree + add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree add_index "namespaces", ["name"], name: "index_namespaces_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"} add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree add_index "namespaces", ["path"], name: "index_namespaces_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"} add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree - add_index "namespaces", ["visibility_level"], name: "index_namespaces_on_visibility_level", using: :btree create_table "notes", force: :cascade do |t| t.text "note" @@ -796,7 +757,6 @@ add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree - add_index "notes", ["created_at", "id"], name: "index_notes_on_created_at_and_id", using: :btree add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree add_index "notes", ["note"], name: "index_notes_on_note_trigram", using: :gin, opclasses: {"note"=>"gin_trgm_ops"} @@ -972,9 +932,8 @@ t.boolean "repository_read_only" end - add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree - add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree + add_index "projects", ["created_at"], name: "index_projects_on_created_at", using: :btree add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree add_index "projects", ["description"], name: "index_projects_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree @@ -1094,9 +1053,6 @@ t.boolean "wiki_page_events", default: true end - add_index "services", ["category"], name: "index_services_on_category", using: :btree - add_index "services", ["created_at", "id"], name: "index_services_on_created_at_and_id", using: :btree - add_index "services", ["default"], name: "index_services_on_default", using: :btree add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree add_index "services", ["template"], name: "index_services_on_template", using: :btree @@ -1113,8 +1069,6 @@ end add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree - add_index "snippets", ["created_at", "id"], name: "index_snippets_on_created_at_and_id", using: :btree - add_index "snippets", ["created_at"], name: "index_snippets_on_created_at", using: :btree add_index "snippets", ["file_name"], name: "index_snippets_on_file_name_trigram", using: :gin, opclasses: {"file_name"=>"gin_trgm_ops"} add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree add_index "snippets", ["title"], name: "index_snippets_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"} @@ -1183,7 +1137,6 @@ add_index "todos", ["commit_id"], name: "index_todos_on_commit_id", using: :btree add_index "todos", ["note_id"], name: "index_todos_on_note_id", using: :btree add_index "todos", ["project_id"], name: "index_todos_on_project_id", using: :btree - add_index "todos", ["state"], name: "index_todos_on_state", using: :btree add_index "todos", ["target_type", "target_id"], name: "index_todos_on_target_type_and_target_id", using: :btree add_index "todos", ["user_id"], name: "index_todos_on_user_id", using: :btree @@ -1265,7 +1218,7 @@ add_index "users", ["admin"], name: "index_users_on_admin", using: :btree add_index "users", ["authentication_token"], name: "index_users_on_authentication_token", unique: true, using: :btree add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree - add_index "users", ["created_at", "id"], name: "index_users_on_created_at_and_id", using: :btree + add_index "users", ["created_at"], name: "index_users_on_created_at", using: :btree add_index "users", ["current_sign_in_at"], name: "index_users_on_current_sign_in_at", using: :btree add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree add_index "users", ["email"], name: "index_users_on_email_trigram", using: :gin, opclasses: {"email"=>"gin_trgm_ops"} @@ -1306,7 +1259,6 @@ t.boolean "wiki_page_events", default: false, null: false end - add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree add_foreign_key "path_locks", "projects" diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index b5db575477c25669bd6ea1b8cf4efc10b3942045..28c4c7c86ca4c1fe543c7b973b0205769155353f 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -121,6 +121,10 @@ Registry is exposed to the outside world is `4567`, here is what you need to set in `gitlab.rb` or `gitlab.yml` if you are using Omnibus GitLab or installed GitLab from source respectively. +>**Note:** +Be careful to choose a port different than the one that Registry listens to (`5000` by default), +otherwise you will run into conflicts . + --- **Omnibus GitLab installations** diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md index f6153216f333a9a4d0c573415659d28084e468fb..bc42433065609965a4e3c73db559544afd00bc9d 100644 --- a/doc/administration/high_availability/redis.md +++ b/doc/administration/high_availability/redis.md @@ -1,7 +1,12 @@ # Configuring Redis for GitLab HA -You can choose to install and manage Redis yourself, or you can use GitLab -Omnibus packages to help. +You can choose to install and manage Redis yourself, or you can use the one +that comes bundled with GitLab Omnibus packages. + +> **Note:** Redis does not require authentication by default. See + [Redis Security](http://redis.io/topics/security) documentation for more + information. We recommend using a combination of a Redis password and tight + firewall rules to secure your Redis service. ## Configure your own Redis server @@ -9,49 +14,293 @@ If you're hosting GitLab on a cloud provider, you can optionally use a managed service for Redis. For example, AWS offers a managed ElastiCache service that runs Redis. -> **Note:** Redis does not require authentication by default. See - [Redis Security](http://redis.io/topics/security) documentation for more - information. We recommend using a combination of a Redis password and tight - firewall rules to secure your Redis service. +## Configure Redis using Omnibus -## Configure using Omnibus +If you don't want to bother setting up your own Redis server, you can use the +one bundled with Omnibus. In this case, you should disable all services except +Redis. 1. Download/install GitLab Omnibus using **steps 1 and 2** from [GitLab downloads](https://about.gitlab.com/downloads). Do not complete other steps on the download page. 1. Create/edit `/etc/gitlab/gitlab.rb` and use the following configuration. Be sure to change the `external_url` to match your eventual GitLab front-end - URL. + URL: ```ruby - external_url 'https://gitlab.example.com' + external_url 'https://gitlab.example.com' - # Disable all components except Redis - redis['enable'] = true - bootstrap['enable'] = false - nginx['enable'] = false - unicorn['enable'] = false - sidekiq['enable'] = false - postgresql['enable'] = false - gitlab_workhorse['enable'] = false - mailroom['enable'] = false + # Disable all services except Redis + redis['enable'] = true + bootstrap['enable'] = false + nginx['enable'] = false + unicorn['enable'] = false + sidekiq['enable'] = false + postgresql['enable'] = false + gitlab_workhorse['enable'] = false + mailroom['enable'] = false - # Redis configuration - redis['port'] = 6379 - redis['bind'] = '0.0.0.0' + # Redis configuration + redis['port'] = 6379 + redis['bind'] = '0.0.0.0' - # If you wish to use Redis authentication (recommended) - redis['password'] = 'Redis Password' + # If you wish to use Redis authentication (recommended) + redis['password'] = 'Redis Password' ``` 1. Run `sudo gitlab-ctl reconfigure` to install and configure PostgreSQL. > **Note**: This `reconfigure` step will result in some errors. That's OK - don't be alarmed. + 1. Run `touch /etc/gitlab/skip-auto-migrations` to prevent database migrations from running on upgrade. Only the primary GitLab application server should handle migrations. +## Experimental Redis Sentinel support + +> [Introduced][ce-1877] in GitLab 8.11. + +Since GitLab 8.11, you can configure a list of Redis Sentinel servers that +will monitor a group of Redis servers to provide you with a standard failover +support. + +There is currently one exception to the Sentinel support: `mail_room`, the +component that processes incoming emails. It doesn't support Sentinel yet, but +we hope to integrate a future release that does support it. + +To get a better understanding on how to correctly setup Sentinel, please read +the [Redis Sentinel documentation](http://redis.io/topics/sentinel) first, as +failing to configure it correctly can lead to data loss. + +The configuration consists of three parts: + +- Redis setup +- Sentinel setup +- GitLab setup + +Read carefully how to configure those components below. + +### Redis setup + +You must have at least 2 Redis servers: 1 Master, 1 or more Slaves. +They should be configured the same way and with similar server specs, as +in a failover situation, any Slave can be elected as the new Master by +the Sentinel servers. + +In a minimal setup, the only required change for the slaves in `redis.conf` +is the addition of a `slaveof` line pointing to the initial master. +You can increase the security by defining a `requirepass` configuration in +the master, and `masterauth` in slaves. + +--- + +**Configuring your own Redis server** + +1. Add to the slaves' `redis.conf`: + + ```conf + # IP and port of the master Redis server + slaveof 10.10.10.10 6379 + ``` + +1. Optionally, set up password authentication for increased security. + Add the following to master's `redis.conf`: + + ```conf + # Optional password authentication for increased security + requirepass "" + ``` + +1. Then add this line to all the slave servers' `redis.conf`: + + ```conf + masterauth "" + ``` + +1. Restart the Redis services for the changes to take effect. + +--- + +**Using Redis via Omnibus** + +1. Edit `/etc/gitlab/gitlab.rb` of a master Redis machine (usualy a single machine): + + ```ruby + ## Redis TCP support (will disable UNIX socket transport) + redis['bind'] = '0.0.0.0' # or specify an IP to bind to a single one + redis['port'] = 6379 + + ## Master redis instance + redis['password'] = '' + ``` + +1. Edit `/etc/gitlab/gitlab.rb` of a slave Redis machine (should be one or more machines): + + ```ruby + ## Redis TCP support (will disable UNIX socket transport) + redis['bind'] = '0.0.0.0' # or specify an IP to bind to a single one + redis['port'] = 6379 + + ## Slave redis instance + redis['master_ip'] = '10.10.10.10' # IP of master Redis server + redis['master_port'] = 6379 # Port of master Redis server + redis['master_password'] = "" + ``` + +1. Reconfigure the GitLab for the changes to take effect: `sudo gitlab-ctl reconfigure` + +--- + +Now that the Redis servers are all set up, let's configure the Sentinel +servers. + +### Sentinel setup + +We don't provide yet an automated way to setup and run the Sentinel daemon +from Omnibus installation method. You must follow the instructions below and +run it by yourself. + +The support for Sentinel in Ruby has some [caveats](https://github.com/redis/redis-rb/issues/531). +While you can give any name for the `master-group-name` part of the +configuration, as in this example: + +```conf +sentinel monitor +``` + +,for it to work in Ruby, you have to use the "hostname" of the master Redis +server, otherwise you will get an error message like: +`Redis::CannotConnectError: No sentinels available.`. Read +[Sentinel troubleshooting](#sentinel-troubleshooting) for more information. + +Here is an example configuration file (`sentinel.conf`) for a Sentinel node: + +```conf +port 26379 +sentinel monitor master-redis.example.com 10.10.10.10 6379 1 +sentinel down-after-milliseconds master-redis.example.com 10000 +sentinel config-epoch master-redis.example.com 0 +sentinel leader-epoch master-redis.example.com 0 +``` + +--- + +The final part is to inform the main GitLab application server of the Redis +master and the new sentinels servers. + +### GitLab setup + +You can enable or disable sentinel support at any time in new or existing +installations. From the GitLab application perspective, all it requires is +the correct credentials for the master Redis and for a few Sentinel nodes. + +It doesn't require a list of all Sentinel nodes, as in case of a failure, +the application will need to query only one of them. + +>**Note:** +The following steps should be performed in the [GitLab application server](gitlab.md). + +**For source based installations** + +1. Edit `/home/git/gitlab/config/resque.yml` following the example in + `/home/git/gitlab/config/resque.yml.example`, and uncomment the sentinels + line, changing to the correct server credentials. +1. Restart GitLab for the changes to take effect. + +**For Omnibus installations** + +1. Edit `/etc/gitlab/gitlab.rb` and add/change the following lines: + + ```ruby + gitlab-rails['redis_host'] = "master-redis.example.com" + gitlab-rails['redis_port'] = 6379 + gitlab-rails['redis_password'] = '' + gitlab-rails['redis_sentinels'] = [ + {'host' => '10.10.10.1', 'port' => 26379}, + {'host' => '10.10.10.2', 'port' => 26379}, + {'host' => '10.10.10.3', 'port' => 26379} + ] + ``` + +1. [Reconfigure] the GitLab for the changes to take effect. + +### Sentinel troubleshooting + +If you get an error like: `Redis::CannotConnectError: No sentinels available.`, +there may be something wrong with your configuration files or it can be related +to [this issue][gh-531] ([pull request][gh-534] that should make things better). + +It's a bit rigid the way you have to config `resque.yml` and `sentinel.conf`, +otherwise `redis-rb` will not work properly. + +The hostname ('my-primary-redis') of the primary Redis server (`sentinel.conf`) +**must** match the one configured in GitLab (`resque.yml` for source installations +or `gitlab-rails['redis_*']` in Omnibus) and it must be valid ex: + +```conf +# sentinel.conf: +sentinel monitor my-primary-redis 10.10.10.10 6379 1 +sentinel down-after-milliseconds my-primary-redis 10000 +sentinel config-epoch my-primary-redis 0 +sentinel leader-epoch my-primary-redis 0 +``` + +```yaml +# resque.yaml +production: + url: redis://my-primary-redis:6378 + sentinels: + - + host: slave1 + port: 26380 # point to sentinel, not to redis port + - + host: slave2 + port: 26381 # point to sentinel, not to redis port +``` + +When in doubt, please read [Redis Sentinel documentation](http://redis.io/topics/sentinel) + +--- + +To make sure your configuration is correct: + +1. SSH into your GitLab application server +1. Enter the Rails console: + + ``` + # For Omnibus installations + sudo gitlab-rails console + + # For source installations + sudo -u git rails console RAILS_ENV=production + ``` + +1. Run in the console: + + ```ruby + redis = Redis.new(Gitlab::Redis.params) + redis.info + ``` + + Keep this screen open and try to simulate a failover below. + +1. To simulate a failover on master Redis, SSH into the Redis server and run: + + ```bash + # port must match your master redis port + redis-cli -h localhost -p 6379 DEBUG sleep 60 + ``` + +1. Then back in the Rails console from the first step, run: + + ``` + redis.info + ``` + + You should see a different port after a few seconds delay + (the failover/reconnect time). + --- Read more on high-availability configuration: @@ -60,3 +309,9 @@ Read more on high-availability configuration: 1. [Configure NFS](nfs.md) 1. [Configure the GitLab application servers](gitlab.md) 1. [Configure the load balancers](load_balancer.md) + +[ce-1877]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1877 +[restart]: ../restart_gitlab.md#installations-from-source +[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure +[gh-531]: https://github.com/redis/redis-rb/issues/531 +[gh-534]: https://github.com/redis/redis-rb/issues/534 diff --git a/doc/api/README.md b/doc/api/README.md index b66d5b158161855ae2b33c5a16b1837c8b3ed428..89007436071c447acfefd3fc4f842e7e6506d732 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -16,6 +16,8 @@ following locations: - [Commits](commits.md) - [Deploy Keys](deploy_keys.md) - [Groups](groups.md) +- [Group Access Requests](access_requests.md) +- [Group Members](members.md) - [Issues](issues.md) - [Keys](keys.md) - [Labels](labels.md) @@ -27,6 +29,8 @@ following locations: - [Notes](notes.md) (comments) - [Open source license templates](licenses.md) - [Projects](projects.md) including setting Webhooks +- [Project Access Requests](access_requests.md) +- [Project Members](members.md) - [Project Snippets](project_snippets.md) - [Repositories](repositories.md) - [Repository Files](repository_files.md) @@ -77,7 +81,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 -H "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v3/projects +curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v3/projects ``` Read more about [GitLab as an OAuth2 client](oauth2.md). @@ -157,7 +161,7 @@ be returned with status code `403`: ```json { - "message": "403 Forbidden: Must be admin to use sudo" + "message": "403 Forbidden - Must be admin to use sudo" } ``` @@ -207,7 +211,7 @@ resources you can pass the following parameters: In the example below, we list 50 [namespaces](namespaces.md) per page. ```bash -curl -X PUT -H "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/v3/namespaces?per_page=50 ``` ### Pagination Link header @@ -221,7 +225,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 -I -H "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/v3/projects/8/issues/8/notes?per_page=3&page=2 ``` The response will then be: diff --git a/doc/api/access_requests.md b/doc/api/access_requests.md new file mode 100644 index 0000000000000000000000000000000000000000..ea308b54d62d6d79a996b4f254fb23b02c3b6eec --- /dev/null +++ b/doc/api/access_requests.md @@ -0,0 +1,147 @@ +# Group and project access requests + + >**Note:** This feature was introduced in GitLab 8.11 + + **Valid access levels** + + The access levels are defined in the `Gitlab::Access` module. Currently, these levels are recognized: + +``` +10 => Guest access +20 => Reporter access +30 => Developer access +40 => Master access +50 => Owner access # Only valid for groups +``` + +## List access requests for a group or project + +Gets a list of access requests viewable by the authenticated user. + +Returns `200` if the request succeeds. + +``` +GET /groups/:id/access_requests +GET /projects/:id/access_requests +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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 +``` + +Example response: + +```json +[ + { + "id": 1, + "username": "raymond_smith", + "name": "Raymond Smith", + "state": "active", + "created_at": "2012-10-22T14:13:35Z", + "requested_at": "2012-10-22T14:13:35Z" + }, + { + "id": 2, + "username": "john_doe", + "name": "John Doe", + "state": "active", + "created_at": "2012-10-22T14:13:35Z", + "requested_at": "2012-10-22T14:13:35Z" + } +] +``` + +## Request access to a group or project + +Requests access for the authenticated user to a group or project. + +Returns `201` if the request succeeds. + +``` +POST /groups/:id/access_requests +POST /projects/:id/access_requests +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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 +``` + +Example response: + +```json +{ + "id": 1, + "username": "raymond_smith", + "name": "Raymond Smith", + "state": "active", + "created_at": "2012-10-22T14:13:35Z", + "requested_at": "2012-10-22T14:13:35Z" +} +``` + +## Approve an access request + +Approves an access request for the given user. + +Returns `201` if the request succeeds. + +``` +PUT /groups/:id/access_requests/:user_id/approve +PUT /projects/:id/access_requests/:user_id/approve +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The group/project ID or path | +| `user_id` | integer | yes | The user ID of the access requester | +| `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 +``` + +Example response: + +```json +{ + "id": 1, + "username": "raymond_smith", + "name": "Raymond Smith", + "state": "active", + "created_at": "2012-10-22T14:13:35Z", + "access_level": 20 +} +``` + +## Deny an access request + +Denies an access request for the given user. + +Returns `200` if the request succeeds. + +``` +DELETE /groups/:id/access_requests/:user_id +DELETE /projects/:id/access_requests/:user_id +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The group/project ID or path | +| `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 +``` diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md index 158fb189005fecec14ae2033fd9a3230c8ac153d..72ec99b7c56f3f411cd58cace0130ba6c6c09a8f 100644 --- a/doc/api/award_emoji.md +++ b/doc/api/award_emoji.md @@ -25,7 +25,7 @@ Parameters: | `awardable_id` | integer | yes | The ID of an awardable | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji ``` Example Response: @@ -85,7 +85,7 @@ Parameters: | `award_id` | integer | yes | The ID of the award emoji | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/1 +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/1 ``` Example Response: @@ -127,7 +127,7 @@ Parameters: | `name` | string | yes | The name of the emoji, without colons | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji?name=blowfish +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji?name=blowfish ``` Example Response: @@ -170,7 +170,7 @@ Parameters: | `award_id` | integer | yes | The ID of a award_emoji | ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/344 +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/344 ``` Example Response: @@ -217,7 +217,7 @@ Parameters: ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji ``` Example Response: @@ -259,7 +259,7 @@ Parameters: | `award_id` | integer | yes | The ID of the award emoji | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji/2 +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji/2 ``` Example Response: @@ -299,7 +299,7 @@ Parameters: | `name` | string | yes | The name of the emoji, without colons | ```bash -curl -X POST -H "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" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji?name=rocket ``` Example Response: @@ -342,7 +342,7 @@ Parameters: | `award_id` | integer | yes | The ID of a award_emoji | ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/345 +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/345 ``` Example Response: diff --git a/doc/api/branches.md b/doc/api/branches.md index dbe8306c66f42b392419a0d74451ce4af8558a2b..0b5f7778fc764e294114f282aac7f9c41d642227 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 -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches ``` Example response: @@ -57,7 +57,7 @@ GET /projects/:id/repository/branches/:branch | `branch` | string | yes | The name of the branch | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master ``` Example response: @@ -95,7 +95,7 @@ PUT /projects/:id/repository/branches/:branch/protect ``` ```bash -curl -X PUT -H "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/v3/projects/5/repository/branches/master/protect?developers_can_push=true&developers_can_merge=true ``` | Attribute | Type | Required | Description | @@ -140,7 +140,7 @@ PUT /projects/:id/repository/branches/:branch/unprotect ``` ```bash -curl -X PUT -H "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/v3/projects/5/repository/branches/master/unprotect ``` | Attribute | Type | Required | Description | @@ -185,7 +185,7 @@ POST /projects/:id/repository/branches | `ref` | string | yes | The branch name or commit SHA to create branch from | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches?branch_name=newbranch&ref=master" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches?branch_name=newbranch&ref=master" ``` Example response: @@ -230,7 +230,7 @@ It returns `200` if it succeeds, `404` if the branch to be deleted does not exis or `400` for other reasons. In case of an error, an explaining message is provided. ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches/newbranch" +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches/newbranch" ``` Example response: diff --git a/doc/api/build_triggers.md b/doc/api/build_triggers.md index 0881a7d7a90408b433c10c20dfcc34788bf3c3c2..1b7a18401384dd1ae5ff23c156fb57e32c4c384b 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 -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers" +curl --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers" ``` ```json @@ -51,7 +51,7 @@ GET /projects/:id/triggers/:token | `token` | string | yes | The `token` of a trigger | ``` -curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d" +curl --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d" ``` ```json @@ -77,7 +77,7 @@ POST /projects/:id/triggers | `id` | integer | yes | The ID of a project | ``` -curl -X POST -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers" +curl --request POST --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers" ``` ```json @@ -104,7 +104,7 @@ DELETE /projects/:id/triggers/:token | `token` | string | yes | The `token` of a trigger | ``` -curl -X DELETE -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d" +curl --request DELETE --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d" ``` ```json diff --git a/doc/api/build_variables.md b/doc/api/build_variables.md index b96f1bdac8ab0971a5b21573c00555738b6cd85c..a21751a49eabd2c0c2c393d9bb1d40ae502167ae 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 -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" +curl --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" ``` ```json @@ -43,7 +43,7 @@ GET /projects/:id/variables/:key | `key` | string | yes | The `key` of a variable | ``` -curl -H "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/v3/projects/1/variables/TEST_VARIABLE_1" ``` ```json @@ -68,7 +68,7 @@ POST /projects/:id/variables | `value` | string | yes | The `value` of a variable | ``` -curl -X POST -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" -F "key=NEW_VARIABLE" -F "value=new value" +curl --request POST --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/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 -X PUT -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/NEW_VARIABLE" -F "value=updated value" +curl --request PUT --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/NEW_VARIABLE" --form "value=updated value" ``` ```json @@ -117,7 +117,7 @@ DELETE /projects/:id/variables/:key | `key` | string | yes | The `key` of a variable | ``` -curl -X DELETE -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/VARIABLE_1" +curl --request DELETE --header "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/VARIABLE_1" ``` ```json diff --git a/doc/api/builds.md b/doc/api/builds.md index 24d90e22a9b67615f7e1a795842277c384217eda..8864df03c98c4efcecd2ee8e2fcc82c811478899 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: `pending`, `running`, `failed`, `success`, `canceled`; showing all builds if none provided | ``` -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds" ``` Example of response @@ -123,7 +123,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: `pending`, `running`, `failed`, `success`, `canceled`; showing all builds if none provided | ``` -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/repository/commits/0ff3ae198f8601a285adcf5c0fff204ee6fba5fd/builds" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/repository/commits/0ff3ae198f8601a285adcf5c0fff204ee6fba5fd/builds" ``` Example of response @@ -209,7 +209,7 @@ GET /projects/:id/builds/:build_id | `build_id` | integer | yes | The ID of a build | ``` -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8" ``` Example of response @@ -271,7 +271,7 @@ GET /projects/:id/builds/:build_id/artifacts | `build_id` | integer | yes | The ID of a build | ``` -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8/artifacts" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8/artifacts" ``` Response: @@ -305,7 +305,7 @@ Parameters Example request: ``` -curl -H "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/v3/projects/1/builds/artifacts/master/download?job=test" ``` Example response: @@ -331,7 +331,7 @@ GET /projects/:id/builds/:build_id/trace | build_id | integer | yes | The ID of a build | ``` -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8/trace" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8/trace" ``` Response: @@ -355,7 +355,7 @@ POST /projects/:id/builds/:build_id/cancel | `build_id` | integer | yes | The ID of a build | ``` -curl -X POST -H "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/v3/projects/1/builds/1/cancel" ``` Example of response @@ -401,7 +401,7 @@ POST /projects/:id/builds/:build_id/retry | `build_id` | integer | yes | The ID of a build | ``` -curl -X POST -H "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/v3/projects/1/builds/1/retry" ``` Example of response @@ -451,7 +451,7 @@ Parameters Example of request ``` -curl -X POST -H "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/v3/projects/1/builds/1/erase" ``` Example of response @@ -501,7 +501,7 @@ Parameters Example request: ``` -curl -X POST -H "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/v3/projects/1/builds/1/artifacts/keep" ``` Example response: diff --git a/doc/api/ci/builds.md b/doc/api/ci/builds.md index d779463fd8cb047f17669399fece03f1249cc43f..2a71b087f193a091d5354c604b7cd6d0f5d1f692 100644 --- a/doc/api/ci/builds.md +++ b/doc/api/ci/builds.md @@ -35,7 +35,7 @@ POST /ci/api/v1/builds/register ``` -curl -X POST "https://gitlab.example.com/ci/api/v1/builds/register" -F "token=t0k3n" +curl --request POST "https://gitlab.example.com/ci/api/v1/builds/register" --form "token=t0k3n" ``` ### Update details of an existing build @@ -52,7 +52,7 @@ PUT /ci/api/v1/builds/:id | `trace` | string | no | The trace of a build | ``` -curl -X PUT "https://gitlab.example.com/ci/api/v1/builds/1234" -F "token=t0k3n" -F "state=running" -F "trace=Running git clone...\n" +curl --request PUT "https://gitlab.example.com/ci/api/v1/builds/1234" --form "token=t0k3n" --form "state=running" --form "trace=Running git clone...\n" ``` ### Incremental build trace update @@ -87,7 +87,7 @@ Headers: | `Content-Range` | string | yes | Bytes range of trace that is sent | ``` -curl -X PATCH "https://gitlab.example.com/ci/api/v1/builds/1234/trace.txt" -H "BUILD-TOKEN=build_t0k3n" -H "Content-Range=0-21" -d "Running git clone...\n" +curl --request PATCH "https://gitlab.example.com/ci/api/v1/builds/1234/trace.txt" --header "BUILD-TOKEN=build_t0k3n" --header "Content-Range=0-21" --data "Running git clone...\n" ``` @@ -104,7 +104,7 @@ POST /ci/api/v1/builds/:id/artifacts | `file` | mixed | yes | Artifacts file | ``` -curl -X POST "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n" -F "file=@/path/to/file" +curl --request POST "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" --form "token=build_t0k3n" --form "file=@/path/to/file" ``` ### Download the artifacts file from build @@ -119,7 +119,7 @@ GET /ci/api/v1/builds/:id/artifacts | `token` | string | yes | The build authorization token | ``` -curl "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n" +curl "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" --form "token=build_t0k3n" ``` ### Remove the artifacts file from build @@ -134,5 +134,5 @@ DELETE /ci/api/v1/builds/:id/artifacts | `token` | string | yes | The build authorization token | ``` -curl -X DELETE "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n" +curl --request DELETE "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" --form "token=build_t0k3n" ``` diff --git a/doc/api/ci/runners.md b/doc/api/ci/runners.md index 96b3c42f773a69b8f1d4e4f9bdd6802a9ed9ca60..ecec53fde0371879031fd6b71a608f100f431c13 100644 --- a/doc/api/ci/runners.md +++ b/doc/api/ci/runners.md @@ -35,7 +35,7 @@ POST /ci/api/v1/runners/register Example request: ```sh -curl -X POST "https://gitlab.example.com/ci/api/v1/runners/register" -F "token=t0k3n" +curl --request POST "https://gitlab.example.com/ci/api/v1/runners/register" --form "token=t0k3n" ``` ## Delete a Runner @@ -53,5 +53,5 @@ DELETE /ci/api/v1/runners/delete Example request: ```sh -curl -X DELETE "https://gitlab.example.com/ci/api/v1/runners/delete" -F "token=t0k3n" +curl --request DELETE "https://gitlab.example.com/ci/api/v1/runners/delete" --form "token=t0k3n" ``` diff --git a/doc/api/commits.md b/doc/api/commits.md index 2960c2ae428db0dd2514de264147697d930a52bb..5c98c5d7565b5820a72af26bc05751b39152f900 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 in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits" ``` Example response: @@ -62,7 +62,7 @@ Parameters: | `sha` | string | yes | The commit hash or name of a repository branch or tag | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master ``` Example response: @@ -106,7 +106,7 @@ Parameters: | `sha` | string | yes | The commit hash or name of a repository branch or tag | ```bash -curl -H "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/v3/projects/5/repository/commits/master/diff" ``` Example response: @@ -142,7 +142,7 @@ Parameters: | `sha` | string | yes | The commit hash or name of a repository branch or tag | ```bash -curl -H "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/v3/projects/5/repository/commits/master/comments" ``` Example response: @@ -195,7 +195,7 @@ POST /projects/:id/repository/commits/:sha/comments | `line_type` | string | no | The line type. Takes `new` or `old` as arguments | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -F "note=Nice picture man\!" -F "path=dudeism.md" -F "line=11" -F "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/v3/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/comments ``` Example response: @@ -240,7 +240,7 @@ GET /projects/:id/repository/commits/:sha/statuses | `all` | boolean | no | Return all statuses, not only the latest ones ```bash -curl -H "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/v3/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/statuses ``` Example response: @@ -315,7 +315,7 @@ POST /projects/:id/statuses/:sha | `description` | string | no | The short description of the status ```bash -curl -X POST -H "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/v3/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 9280f0d68b62ec3b128ae52f27f71ad012f28797..73cb4b7ea8c277ef2cc5e7e95c8cdf423fff52b2 100644 --- a/doc/api/deploy_key_multiple_projects.md +++ b/doc/api/deploy_key_multiple_projects.md @@ -7,23 +7,23 @@ First, find the ID of the projects you're interested in, by either listing all projects: ``` -curl -H 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/projects +curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/projects ``` Or finding the ID of a group and then listing all projects in that group: ``` -curl -H 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/groups +curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/groups # For group 1234: -curl -H 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/groups/1234 +curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/groups/1234 ``` With those IDs, add the same deploy key to all: ``` for project_id in 321 456 987; do - curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -H "Content-Type: application/json" \ + 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 done ``` diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md index 4e620ccc81a5967e760fdf8000d96510f17bb2d6..ca44afbf355bde14d960c2fe9f746c509ef7d7fb 100644 --- a/doc/api/deploy_keys.md +++ b/doc/api/deploy_keys.md @@ -9,7 +9,7 @@ GET /deploy_keys ``` ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/deploy_keys" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/deploy_keys" ``` Example response: @@ -44,7 +44,7 @@ GET /projects/:id/deploy_keys | `id` | integer | yes | The ID of the project | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys" ``` Example response: @@ -82,7 +82,7 @@ Parameters: | `key_id` | integer | yes | The ID of the deploy key | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys/11" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys/11" ``` Example response: @@ -114,7 +114,7 @@ POST /projects/:id/deploy_keys | `key` | string | yes | New deploy key | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -H "Content-Type: application/json" --data '{"title": "My deploy key", "key": "ssh-rsa AAAA..."}' "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..."}' "https://gitlab.example.com/api/v3/projects/5/deploy_keys/" ``` Example response: @@ -142,7 +142,7 @@ DELETE /projects/:id/deploy_keys/:key_id | `key_id` | integer | yes | The ID of the deploy key | ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys/13" +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys/13" ``` Example response: @@ -159,3 +159,51 @@ Example response: "id" : 13 } ``` + +## Enable a deploy key + +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 +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project | +| `key_id` | integer | yes | The ID of the deploy key | + +Example response: + +```json +{ + "key" : "ssh-rsa AAAA...", + "id" : 12, + "title" : "My deploy key", + "created_at" : "2015-08-29T12:44:31.550Z" +} +``` + +## Disable a deploy key + +Disable a deploy key for a project. Returns the disabled key. + +```bash +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/deploy_keys/13/disable +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project | +| `key_id` | integer | yes | The ID of the deploy key | + +Example response: + +```json +{ + "key" : "ssh-rsa AAAA...", + "id" : 12, + "title" : "My deploy key", + "created_at" : "2015-08-29T12:44:31.550Z" +} +``` diff --git a/doc/api/enviroments.md b/doc/api/enviroments.md index 1e12ded448c09b5c281ede3d84b82bfb0b684310..87a5fa67124996dcf9e80d93800ecb6d581b7353 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 -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/environments +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/environments ``` Example response: @@ -45,7 +45,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" -H "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/v3/projects/1/environments" ``` Example response: @@ -76,7 +76,7 @@ PUT /projects/:id/environments/:environments_id | `external_url` | string | no | The new external_url | ```bash -curl -X PUT --data "name=staging&external_url=https://staging.example.gitlab.com" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environment/1" +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/environment/1" ``` Example response: @@ -103,7 +103,7 @@ DELETE /projects/:id/environments/:environment_id | `environment_id` | integer | yes | The ID of the environment | ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environment/1" +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environment/1" ``` Example response: diff --git a/doc/api/groups.md b/doc/api/groups.md index a9f2ad00eea82064cd12c75aa1b35e56d485b1f1..a898387eaa2a6887ee7deedd3468462af8445b69 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -1,571 +1,434 @@ -# Groups - -## List groups - -Get a list of groups. (As user: my groups, as admin: all groups) - -``` -GET /groups -``` - -```json -[ - { - "id": 1, - "name": "Foobar Group", - "path": "foo-bar", - "description": "An interesting group" - } -] -``` - -You can search for groups by name or path, see below. - -## List a group's projects - -Get a list of projects in this group. - -``` -GET /groups/:id/projects -``` - -Parameters: - -- `archived` (optional) - if passed, limit by archived status -- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private` -- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at` -- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` -- `search` (optional) - Return list of authorized projects according to a search criteria -- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first - -```json -[ - { - "id": 9, - "description": "foo", - "default_branch": "master", - "tag_list": [], - "public": false, - "archived": false, - "visibility_level": 10, - "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", - "name": "Html5 Boilerplate", - "name_with_namespace": "Experimental / Html5 Boilerplate", - "path": "html5-boilerplate", - "path_with_namespace": "h5bp/html5-boilerplate", - "issues_enabled": true, - "merge_requests_enabled": true, - "wiki_enabled": true, - "builds_enabled": true, - "snippets_enabled": true, - "created_at": "2016-04-05T21:40:50.169Z", - "last_activity_at": "2016-04-06T16:52:08.432Z", - "shared_runners_enabled": true, - "creator_id": 1, - "namespace": { - "id": 5, - "name": "Experimental", - "path": "h5bp", - "owner_id": null, - "created_at": "2016-04-05T21:40:49.152Z", - "updated_at": "2016-04-07T08:07:48.466Z", - "description": "foo", - "avatar": { - "url": null - }, - "share_with_group_lock": false, - "visibility_level": 10 - }, - "avatar_url": null, - "star_count": 1, - "forks_count": 0, - "open_issues_count": 3, - "public_builds": true, - "shared_with_groups": [] - } -] -``` - -## Details of a group - -Get all details of a group. - -``` -GET /groups/:id -``` - -Parameters: - -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or path of a group | - -```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/4 -``` - -Example response: - -```json -{ - "id": 4, - "name": "Twitter", - "path": "twitter", - "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.", - "visibility_level": 20, - "avatar_url": null, - "web_url": "https://gitlab.example.com/groups/twitter", - "projects": [ - { - "id": 7, - "description": "Voluptas veniam qui et beatae voluptas doloremque explicabo facilis.", - "default_branch": "master", - "tag_list": [], - "public": true, - "archived": false, - "visibility_level": 20, - "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", - "name": "Typeahead.Js", - "name_with_namespace": "Twitter / Typeahead.Js", - "path": "typeahead-js", - "path_with_namespace": "twitter/typeahead-js", - "issues_enabled": true, - "merge_requests_enabled": true, - "wiki_enabled": true, - "builds_enabled": true, - "snippets_enabled": false, - "container_registry_enabled": true, - "created_at": "2016-06-17T07:47:25.578Z", - "last_activity_at": "2016-06-17T07:47:25.881Z", - "shared_runners_enabled": true, - "creator_id": 1, - "namespace": { - "id": 4, - "name": "Twitter", - "path": "twitter", - "owner_id": null, - "created_at": "2016-06-17T07:47:24.216Z", - "updated_at": "2016-06-17T07:47:24.216Z", - "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.", - "avatar": { - "url": null - }, - "share_with_group_lock": false, - "visibility_level": 20 - }, - "avatar_url": null, - "star_count": 0, - "forks_count": 0, - "open_issues_count": 3, - "public_builds": true, - "shared_with_groups": [] - }, - { - "id": 6, - "description": "Aspernatur omnis repudiandae qui voluptatibus eaque.", - "default_branch": "master", - "tag_list": [], - "public": false, - "archived": false, - "visibility_level": 10, - "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", - "name": "Flight", - "name_with_namespace": "Twitter / Flight", - "path": "flight", - "path_with_namespace": "twitter/flight", - "issues_enabled": true, - "merge_requests_enabled": true, - "wiki_enabled": true, - "builds_enabled": true, - "snippets_enabled": false, - "container_registry_enabled": true, - "created_at": "2016-06-17T07:47:24.661Z", - "last_activity_at": "2016-06-17T07:47:24.838Z", - "shared_runners_enabled": true, - "creator_id": 1, - "namespace": { - "id": 4, - "name": "Twitter", - "path": "twitter", - "owner_id": null, - "created_at": "2016-06-17T07:47:24.216Z", - "updated_at": "2016-06-17T07:47:24.216Z", - "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.", - "avatar": { - "url": null - }, - "share_with_group_lock": false, - "visibility_level": 20 - }, - "avatar_url": null, - "star_count": 0, - "forks_count": 0, - "open_issues_count": 8, - "public_builds": true, - "shared_with_groups": [] - } - ], - "shared_projects": [ - { - "id": 8, - "description": "Velit eveniet provident fugiat saepe eligendi autem.", - "default_branch": "master", - "tag_list": [], - "public": false, - "archived": false, - "visibility_level": 0, - "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", - "name": "Html5 Boilerplate", - "name_with_namespace": "H5bp / Html5 Boilerplate", - "path": "html5-boilerplate", - "path_with_namespace": "h5bp/html5-boilerplate", - "issues_enabled": true, - "merge_requests_enabled": true, - "wiki_enabled": true, - "builds_enabled": true, - "snippets_enabled": false, - "container_registry_enabled": true, - "created_at": "2016-06-17T07:47:27.089Z", - "last_activity_at": "2016-06-17T07:47:27.310Z", - "shared_runners_enabled": true, - "creator_id": 1, - "namespace": { - "id": 5, - "name": "H5bp", - "path": "h5bp", - "owner_id": null, - "created_at": "2016-06-17T07:47:26.621Z", - "updated_at": "2016-06-17T07:47:26.621Z", - "description": "Id consequatur rem vel qui doloremque saepe.", - "avatar": { - "url": null - }, - "share_with_group_lock": false, - "visibility_level": 20 - }, - "avatar_url": null, - "star_count": 0, - "forks_count": 0, - "open_issues_count": 4, - "public_builds": true, - "shared_with_groups": [ - { - "group_id": 4, - "group_name": "Twitter", - "group_access_level": 30 - }, - { - "group_id": 3, - "group_name": "Gitlab Org", - "group_access_level": 10 - } - ] - } - ] -} -``` - -## New group - -Creates a new project group. Available only for users who can create groups. - -``` -POST /groups -``` - -Parameters: - -- `name` (required) - The name of the group -- `path` (required) - The path of the group -- `description` (optional) - The group's description -- `membership_lock` (optional, boolean) - Prevent adding new members to project membership within this group -- `share_with_group_lock` (optional, boolean) - Prevent sharing a project with another group within this group - -## Update group - -Updates a project group. Available only for users who can manage this group. - -``` -PUT /groups/:id -``` - -Parameters: - -- `name` (required) - The name of the group -- `path` (required) - The path of the group -- `description` (optional) - The group's description -- `membership_lock` (optional, boolean) - Prevent adding new members to project membership within this group -- `share_with_group_lock` (optional, boolean) - Prevent sharing a project with another group within this group -- `visibility_level` (optional) - The group's visibility. 0 for private, 10 for internal, 20 for public. - -## Transfer project to group - -Transfer a project to the Group namespace. Available only for admin - -``` -POST /groups/:id/projects/:project_id -``` - -Parameters: - -- `id` (required) - The ID or path of a group -- `project_id` (required) - The ID of a project - -## Update group - -Updates the project group. Only available to group owners and administrators. - -``` -PUT /groups/:id -``` - -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of the group | -| `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. | - -```bash -curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/groups/5?name=Experimental" - -``` - -Example response: - -```json -{ - "id": 5, - "name": "Experimental", - "path": "h5bp", - "description": "foo", - "visibility_level": 10, - "avatar_url": null, - "web_url": "http://gitlab.example.com/groups/h5bp", - "projects": [ - { - "id": 9, - "description": "foo", - "default_branch": "master", - "tag_list": [], - "public": false, - "archived": false, - "visibility_level": 10, - "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", - "name": "Html5 Boilerplate", - "name_with_namespace": "Experimental / Html5 Boilerplate", - "path": "html5-boilerplate", - "path_with_namespace": "h5bp/html5-boilerplate", - "issues_enabled": true, - "merge_requests_enabled": true, - "wiki_enabled": true, - "builds_enabled": true, - "snippets_enabled": true, - "created_at": "2016-04-05T21:40:50.169Z", - "last_activity_at": "2016-04-06T16:52:08.432Z", - "shared_runners_enabled": true, - "creator_id": 1, - "namespace": { - "id": 5, - "name": "Experimental", - "path": "h5bp", - "owner_id": null, - "created_at": "2016-04-05T21:40:49.152Z", - "updated_at": "2016-04-07T08:07:48.466Z", - "description": "foo", - "avatar": { - "url": null - }, - "share_with_group_lock": false, - "visibility_level": 10 - }, - "avatar_url": null, - "star_count": 1, - "forks_count": 0, - "open_issues_count": 3, - "public_builds": true, - "shared_with_groups": [] - } - ] -} -``` - -## Remove group - -Removes group with all projects inside. - -``` -DELETE /groups/:id -``` - -Parameters: - -- `id` (required) - The ID or path of a user group - -## Search for group - -Get all groups that match your string in their name or path. - -``` -GET /groups?search=foobar -``` - -```json -[ - { - "id": 1, - "name": "Foobar Group", - "path": "foo-bar", - "description": "An interesting group" - } -] -``` - -## Group members - -**Group access levels** - -The group access levels are defined in the `Gitlab::Access` module. Currently, these levels are recognized: - -``` -GUEST = 10 -REPORTER = 20 -DEVELOPER = 30 -MASTER = 40 -OWNER = 50 -``` - -### List group members - -Get a list of group members viewable by the authenticated user. - -``` -GET /groups/:id/members -``` - -```json -[ - { - "id": 1, - "username": "raymond_smith", - "name": "Raymond Smith", - "state": "active", - "created_at": "2012-10-22T14:13:35Z", - "access_level": 30 - }, - { - "id": 2, - "username": "john_doe", - "name": "John Doe", - "state": "active", - "created_at": "2012-10-22T14:13:35Z", - "access_level": 30 - } -] -``` - -### Add group member - -Adds a user to the list of group members. - -``` -POST /groups/:id/members -``` - -Parameters: - -- `id` (required) - The ID or path of a group -- `user_id` (required) - The ID of a user to add -- `access_level` (required) - Project access level - -### Edit group team member - -Updates a group team member to a specified access level. - -``` -PUT /groups/:id/members/:user_id -``` - -Parameters: - -- `id` (required) - The ID of a group -- `user_id` (required) - The ID of a group member -- `access_level` (required) - Project access level - -### Remove user team member - -Removes user from user team. - -``` -DELETE /groups/:id/members/:user_id -``` - -Parameters: - -- `id` (required) - The ID or path of a user group -- `user_id` (required) - The ID of a group member - -### Add LDAP group link - -Adds LDAP group link - -``` -POST /groups/:id/ldap_group_links -``` - -Parameters: - -- `id` (required) - The ID of a group -- `cn` (required) - The CN of a LDAP group -- `group_access` (required) - Minimum access level for members of the LDAP group -- `provider` (required) - LDAP provider for the LDAP group (when using several providers) - -### Delete LDAP group link - -Deletes a LDAP group link - -``` -DELETE /groups/:id/ldap_group_links/:cn -``` - -Parameters: - -- `id` (required) - The ID of a group -- `cn` (required) - The CN of a LDAP group - -Deletes a LDAP group link for a specific LDAP provider - -``` -DELETE /groups/:id/ldap_group_links/:provider/:cn -``` - -Parameters: - -- `id` (required) - The ID of a group -- `cn` (required) - The CN of a LDAP group -- `provider` (required) - Name of a LDAP provider - -## Namespaces in groups - -By default, groups only get 20 namespaces at a time because the API results are paginated. - -To get more (up to 100), pass the following as an argument to the API call: -``` -/groups?per_page=100 -``` - -And to switch pages add: -``` -/groups?per_page=100&page=2 -``` +# Groups + +## List groups + +Get a list of groups. (As user: my groups, as admin: all groups) + +``` +GET /groups +``` + +```json +[ + { + "id": 1, + "name": "Foobar Group", + "path": "foo-bar", + "description": "An interesting group" + } +] +``` + +You can search for groups by name or path, see below. + + +## List a group's projects + +Get a list of projects in this group. + +``` +GET /groups/:id/projects +``` + +Parameters: + +- `archived` (optional) - if passed, limit by archived status +- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private` +- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at` +- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` +- `search` (optional) - Return list of authorized projects according to a search criteria +- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first + +```json +[ + { + "id": 9, + "description": "foo", + "default_branch": "master", + "tag_list": [], + "public": false, + "archived": false, + "visibility_level": 10, + "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", + "name": "Html5 Boilerplate", + "name_with_namespace": "Experimental / Html5 Boilerplate", + "path": "html5-boilerplate", + "path_with_namespace": "h5bp/html5-boilerplate", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": true, + "created_at": "2016-04-05T21:40:50.169Z", + "last_activity_at": "2016-04-06T16:52:08.432Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 5, + "name": "Experimental", + "path": "h5bp", + "owner_id": null, + "created_at": "2016-04-05T21:40:49.152Z", + "updated_at": "2016-04-07T08:07:48.466Z", + "description": "foo", + "avatar": { + "url": null + }, + "share_with_group_lock": false, + "visibility_level": 10 + }, + "avatar_url": null, + "star_count": 1, + "forks_count": 0, + "open_issues_count": 3, + "public_builds": true, + "shared_with_groups": [] + } +] +``` + +## Details of a group + +Get all details of a group. + +``` +GET /groups/:id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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 +``` + +Example response: + +```json +{ + "id": 4, + "name": "Twitter", + "path": "twitter", + "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.", + "visibility_level": 20, + "avatar_url": null, + "web_url": "https://gitlab.example.com/groups/twitter", + "projects": [ + { + "id": 7, + "description": "Voluptas veniam qui et beatae voluptas doloremque explicabo facilis.", + "default_branch": "master", + "tag_list": [], + "public": true, + "archived": false, + "visibility_level": 20, + "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", + "name": "Typeahead.Js", + "name_with_namespace": "Twitter / Typeahead.Js", + "path": "typeahead-js", + "path_with_namespace": "twitter/typeahead-js", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "container_registry_enabled": true, + "created_at": "2016-06-17T07:47:25.578Z", + "last_activity_at": "2016-06-17T07:47:25.881Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 4, + "name": "Twitter", + "path": "twitter", + "owner_id": null, + "created_at": "2016-06-17T07:47:24.216Z", + "updated_at": "2016-06-17T07:47:24.216Z", + "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.", + "avatar": { + "url": null + }, + "share_with_group_lock": false, + "visibility_level": 20 + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "open_issues_count": 3, + "public_builds": true, + "shared_with_groups": [] + }, + { + "id": 6, + "description": "Aspernatur omnis repudiandae qui voluptatibus eaque.", + "default_branch": "master", + "tag_list": [], + "public": false, + "archived": false, + "visibility_level": 10, + "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", + "name": "Flight", + "name_with_namespace": "Twitter / Flight", + "path": "flight", + "path_with_namespace": "twitter/flight", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "container_registry_enabled": true, + "created_at": "2016-06-17T07:47:24.661Z", + "last_activity_at": "2016-06-17T07:47:24.838Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 4, + "name": "Twitter", + "path": "twitter", + "owner_id": null, + "created_at": "2016-06-17T07:47:24.216Z", + "updated_at": "2016-06-17T07:47:24.216Z", + "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.", + "avatar": { + "url": null + }, + "share_with_group_lock": false, + "visibility_level": 20 + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "open_issues_count": 8, + "public_builds": true, + "shared_with_groups": [] + } + ], + "shared_projects": [ + { + "id": 8, + "description": "Velit eveniet provident fugiat saepe eligendi autem.", + "default_branch": "master", + "tag_list": [], + "public": false, + "archived": false, + "visibility_level": 0, + "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", + "name": "Html5 Boilerplate", + "name_with_namespace": "H5bp / Html5 Boilerplate", + "path": "html5-boilerplate", + "path_with_namespace": "h5bp/html5-boilerplate", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "container_registry_enabled": true, + "created_at": "2016-06-17T07:47:27.089Z", + "last_activity_at": "2016-06-17T07:47:27.310Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 5, + "name": "H5bp", + "path": "h5bp", + "owner_id": null, + "created_at": "2016-06-17T07:47:26.621Z", + "updated_at": "2016-06-17T07:47:26.621Z", + "description": "Id consequatur rem vel qui doloremque saepe.", + "avatar": { + "url": null + }, + "share_with_group_lock": false, + "visibility_level": 20 + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "open_issues_count": 4, + "public_builds": true, + "shared_with_groups": [ + { + "group_id": 4, + "group_name": "Twitter", + "group_access_level": 30 + }, + { + "group_id": 3, + "group_name": "Gitlab Org", + "group_access_level": 10 + } + ] + } + ] +} +``` + +## New group + +Creates a new project group. Available only for users who can create groups. + +``` +POST /groups +``` + +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. + +## Transfer project to group + +Transfer a project to the Group namespace. Available only for admin + +``` +POST /groups/:id/projects/:project_id +``` + +Parameters: + +- `id` (required) - The ID or path of a group +- `project_id` (required) - The ID of a project + +## Update group + +Updates the project group. Only available to group owners and administrators. + +``` +PUT /groups/:id +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the group | +| `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. | + +```bash +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/groups/5?name=Experimental" + +``` + +Example response: + +```json +{ + "id": 5, + "name": "Experimental", + "path": "h5bp", + "description": "foo", + "visibility_level": 10, + "avatar_url": null, + "web_url": "http://gitlab.example.com/groups/h5bp", + "projects": [ + { + "id": 9, + "description": "foo", + "default_branch": "master", + "tag_list": [], + "public": false, + "archived": false, + "visibility_level": 10, + "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", + "name": "Html5 Boilerplate", + "name_with_namespace": "Experimental / Html5 Boilerplate", + "path": "html5-boilerplate", + "path_with_namespace": "h5bp/html5-boilerplate", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": true, + "created_at": "2016-04-05T21:40:50.169Z", + "last_activity_at": "2016-04-06T16:52:08.432Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 5, + "name": "Experimental", + "path": "h5bp", + "owner_id": null, + "created_at": "2016-04-05T21:40:49.152Z", + "updated_at": "2016-04-07T08:07:48.466Z", + "description": "foo", + "avatar": { + "url": null + }, + "share_with_group_lock": false, + "visibility_level": 10 + }, + "avatar_url": null, + "star_count": 1, + "forks_count": 0, + "open_issues_count": 3, + "public_builds": true, + "shared_with_groups": [] + } + ] +} +``` + +## Remove group + +Removes group with all projects inside. + +``` +DELETE /groups/:id +``` + +Parameters: + +- `id` (required) - The ID or path of a user group + +## Search for group + +Get all groups that match your string in their name or path. + +``` +GET /groups?search=foobar +``` + +```json +[ + { + "id": 1, + "name": "Foobar Group", + "path": "foo-bar", + "description": "An interesting group" + } +] +``` + +## Group members + +Please consult the [Group Members](members.md) documentation. + +## Namespaces in groups + +By default, groups only get 20 namespaces at a time because the API results are paginated. + +To get more (up to 100), pass the following as an argument to the API call: +``` +/groups?per_page=100 +``` + +And to switch pages add: +``` +/groups?per_page=100&page=2 +``` diff --git a/doc/api/issues.md b/doc/api/issues.md index 419fb8f85d8ace0393e8f123e7c1ce77735c34d5..a665645ad0ef618be6f51dc8d6b218891958693a 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -33,7 +33,7 @@ GET /issues?labels=foo,bar&state=opened | `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/issues +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/issues ``` Example response: @@ -110,7 +110,7 @@ GET /groups/:id/issues?milestone=1.0.0&state=opened ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/4/issues +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/4/issues ``` Example response: @@ -189,7 +189,7 @@ GET /projects/:id/issues?iid=42 ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues ``` Example response: @@ -254,7 +254,7 @@ GET /projects/:id/issues/:issue_id | `issue_id`| integer | yes | The ID of a project's issue | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/41 +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/41 ``` Example response: @@ -327,7 +327,7 @@ POST /projects/:id/issues | `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` | ```bash -curl -X POST -H "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/v3/projects/4/issues?title=Issues%20with%20auth&labels=bug ``` Example response: @@ -388,7 +388,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 -X PUT -H "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/v3/projects/4/issues/85?state_event=close ``` Example response: @@ -438,7 +438,7 @@ DELETE /projects/:id/issues/:issue_id | `issue_id` | integer | yes | The ID of a project's issue | ```bash -curl -X DELETE -H "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/v3/projects/4/issues/85 ``` ## Move an issue @@ -463,7 +463,7 @@ POST /projects/:id/issues/:issue_id/move | `to_project_id` | integer | yes | The ID of the new project | ```bash -curl -X POST -H "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/v3/projects/4/issues/85/move ``` Example response: @@ -518,7 +518,7 @@ POST /projects/:id/issues/:issue_id/subscription | `issue_id` | integer | yes | The ID of a project's issue | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscription +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscription ``` Example response: @@ -573,7 +573,7 @@ DELETE /projects/:id/issues/:issue_id/subscription | `issue_id` | integer | yes | The ID of a project's issue | ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscription +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscription ``` Example response: @@ -628,7 +628,7 @@ POST /projects/:id/issues/:issue_id/todo | `issue_id` | integer | yes | The ID of a project's issue | ```bash -curl -X POST -H "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/v3/projects/5/issues/93/todo ``` Example response: diff --git a/doc/api/labels.md b/doc/api/labels.md index a181c0f57a276486d6bf15c2967a1dfa5d5eb4ad..3653ccf304acf21220a37eeccd484bd4fddb40d8 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 -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/labels +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/labels ``` Example response: @@ -82,7 +82,7 @@ POST /projects/:id/labels | `description` | string | no | The description of the label | ```bash -curl --data "name=feature&color=#5843AD" -H "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/v3/projects/1/labels" ``` Example response: @@ -113,7 +113,7 @@ DELETE /projects/:id/labels | `name` | string | yes | The name of the label | ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels?name=bug" +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels?name=bug" ``` Example response: @@ -153,7 +153,7 @@ PUT /projects/:id/labels | `description` | string | no | The new description of the label | ```bash -curl -X PUT --data "name=documentation&new_name=docs&color=#8E44AD&description=Documentation" -H "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/v3/projects/1/labels" ``` Example response: @@ -184,7 +184,7 @@ POST /projects/:id/labels/:label_id/subscription | `label_id` | integer or string | yes | The ID or title of a project's label | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription ``` Example response: @@ -219,7 +219,7 @@ DELETE /projects/:id/labels/:label_id/subscription | `label_id` | integer or string | yes | The ID or title of a project's label | ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription ``` Example response: diff --git a/doc/api/licenses.md b/doc/api/licenses.md index 855b0eab56fe265fdad31585f8ed8e4dade28bd0..ed26d1fb7fbf777c843948148a57abc6fa732d37 100644 --- a/doc/api/licenses.md +++ b/doc/api/licenses.md @@ -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 -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/licenses/mit?project=My+Cool+Project +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/licenses/mit?project=My+Cool+Project ``` Example response: diff --git a/doc/api/members.md b/doc/api/members.md new file mode 100644 index 0000000000000000000000000000000000000000..d002e6eaf89ec27803ea01cb912ea77c1f8a0192 --- /dev/null +++ b/doc/api/members.md @@ -0,0 +1,182 @@ +# Group and project members + +**Valid access levels** + +The access levels are defined in the `Gitlab::Access` module. Currently, these levels are recognized: + +``` +10 => Guest access +20 => Reporter access +30 => Developer access +40 => Master access +50 => Owner access # Only valid for groups +``` + +## List all members of a group or project + +Gets a list of group or project members viewable by the authenticated user. + +Returns `200` if the request succeeds. + +``` +GET /groups/:id/members +GET /projects/:id/members +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The group/project ID or path | +| `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 +``` + +Example response: + +```json +[ + { + "id": 1, + "username": "raymond_smith", + "name": "Raymond Smith", + "state": "active", + "created_at": "2012-10-22T14:13:35Z", + "access_level": 30 + }, + { + "id": 2, + "username": "john_doe", + "name": "John Doe", + "state": "active", + "created_at": "2012-10-22T14:13:35Z", + "access_level": 30 + } +] +``` + +## Get a member of a group or project + +Gets a member of a group or project. + +Returns `200` if the request succeeds. + +``` +GET /groups/:id/members/:user_id +GET /projects/:id/members/:user_id +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The group/project ID or path | +| `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 +``` + +Example response: + +```json +{ + "id": 1, + "username": "raymond_smith", + "name": "Raymond Smith", + "state": "active", + "created_at": "2012-10-22T14:13:35Z", + "access_level": 30 +} +``` + +## Add a member to a group or project + +Adds a member to a group or project. + +Returns `201` if the request succeeds. + +``` +POST /groups/:id/members +POST /projects/:id/members +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The group/project ID or path | +| `user_id` | integer | yes | The user ID of the new member | +| `access_level` | integer | yes | A valid access level | + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id?access_level=30 +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id?access_level=30 +``` + +Example response: + +```json +{ + "id": 1, + "username": "raymond_smith", + "name": "Raymond Smith", + "state": "active", + "created_at": "2012-10-22T14:13:35Z", + "access_level": 30 +} +``` + +## Edit a member of a group or project + +Updates a member of a group or project. + +Returns `200` if the request succeeds. + +``` +PUT /groups/:id/members/:user_id +PUT /projects/:id/members/:user_id +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The group/project ID or path | +| `user_id` | integer | yes | The user ID of the member | +| `access_level` | integer | yes | A valid access level | + +```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 +``` + +Example response: + +```json +{ + "id": 1, + "username": "raymond_smith", + "name": "Raymond Smith", + "state": "active", + "created_at": "2012-10-22T14:13:35Z", + "access_level": 40 +} +``` + +## Remove a member from a group or project + +Removes a user from a group or project. + +Returns `200` if the request succeeds. + +``` +DELETE /groups/:id/members/:user_id +DELETE /projects/:id/members/:user_id +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The group/project ID or path | +| `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 +``` diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 9e0968b21f6da745c06a9f0352a9a50e84cb59a3..ae4250150edbf92fedb30a4b3287bbcfa7d2da1c 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -433,7 +433,7 @@ DELETE /projects/:id/merge_requests/:merge_request_id | `merge_request_id` | integer | yes | The ID of a project's merge request | ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/merge_request/85 +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/merge_request/85 ``` ## Accept MR @@ -706,7 +706,7 @@ GET /projects/:id/merge_requests/:merge_request_id/closes_issues | `merge_request_id` | integer | yes | The ID of the merge request | ```bash -curl -H "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/v3/projects/76/merge_requests/1/closes_issues ``` Example response when the GitLab issue tracker is used: @@ -785,7 +785,7 @@ POST /projects/:id/merge_requests/:merge_request_id/subscription | `merge_request_id` | integer | yes | The ID of the merge request | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription ``` Example response: @@ -859,7 +859,7 @@ DELETE /projects/:id/merge_requests/:merge_request_id/subscription | `merge_request_id` | integer | yes | The ID of the merge request | ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription ``` Example response: @@ -932,7 +932,7 @@ POST /projects/:id/merge_requests/:merge_request_id/todo | `merge_request_id` | integer | yes | The ID of the merge request | ```bash -curl -X POST -H "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/v3/projects/5/merge_requests/27/todo ``` Example response: diff --git a/doc/api/milestones.md b/doc/api/milestones.md index e4202025f8022a2cad6dc6e1d701aab5a44dcdf4..ae7d22a4be554dc0dffd5feab4bde8466efd2f6e 100644 --- a/doc/api/milestones.md +++ b/doc/api/milestones.md @@ -20,7 +20,7 @@ Parameters: | `state` | string | optional | Return only `active` or `closed` milestones` | ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/milestones +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/milestones ``` Example Response: diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md index 42d9ce3d3915d866ece9f536d38cdb9ab55e36a8..88cd407d792b3e3ecfd334e698a173e8f8d4dbfe 100644 --- a/doc/api/namespaces.md +++ b/doc/api/namespaces.md @@ -19,7 +19,7 @@ GET /namespaces Example request: ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/namespaces +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/namespaces ``` Example response: @@ -54,7 +54,7 @@ GET /namespaces?search=foobar Example request: ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/namespaces?search=twitter +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/namespaces?search=twitter ``` Example response: diff --git a/doc/api/notes.md b/doc/api/notes.md index 7aa1c2155bfe8d5c2c2b5f30ffd0002ac71115a4..85d140d06acfe5818232df1624c7f2802238bd71 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -124,7 +124,7 @@ Parameters: | `note_id` | integer | yes | The ID of a note | ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/11/notes/636 +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/11/notes/636 ``` Example Response: @@ -248,7 +248,7 @@ Parameters: | `note_id` | integer | yes | The ID of a note | ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/snippets/52/notes/1659 +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/snippets/52/notes/1659 ``` Example Response: @@ -376,7 +376,7 @@ Parameters: | `note_id` | integer | yes | The ID of a note | ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/7/notes/1602 +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/7/notes/1602 ``` Example Response: diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md index a24abb249a07b6f75f3c3d9155f5ffff3d739562..f27b43b965988793a8ad01863a76e0b3a6461dcc 100644 --- a/doc/api/oauth2.md +++ b/doc/api/oauth2.md @@ -1,38 +1,59 @@ # GitLab as an OAuth2 client -This document is about using other OAuth authentication service providers to sign into GitLab. -If you want GitLab to be an OAuth authentication service provider to sign into other services please see the [Oauth2 provider documentation](../integration/oauth_provider.md). +This document covers using the OAuth2 protocol to access GitLab. -OAuth2 is a protocol that enables us to authenticate a user without requiring them to give their password. +If you want GitLab to be an OAuth authentication service provider to sign into other services please see the [Oauth2 provider documentation](../integration/oauth_provider.md). -Before using the OAuth2 you should create an application in user's account. Each application gets a unique App ID and App Secret parameters. You should not share these. +OAuth2 is a protocol that enables us to authenticate a user without requiring them to give their password to a third-party. This functionality is based on [doorkeeper gem](https://github.com/doorkeeper-gem/doorkeeper) ## Web Application Flow -This flow is using for authentication from third-party web sites and is probably used the most. -It basically consists of an exchange of an authorization token for an access token. For more detailed info, check out the [RFC spec here](http://tools.ietf.org/html/rfc6749#section-4.1) +This is the most common type of flow and is used by server-side clients that wish to access GitLab on a user's behalf. + +>**Note:** +This flow **should not** be used for client-side clients as you would leak your `client_secret`. Client-side clients should use the Implicit Grant (which is currently unsupported). -This flow consists from 3 steps. +For more detailed information, check out the [RFC spec](http://tools.ietf.org/html/rfc6749#section-4.1) + +In the following sections you will be introduced to the three steps needed for this flow. ### 1. Registering the client -Create an application in user's account profile. +First, you should create an application (`/profile/applications`) in your user's account. +Each application gets a unique App ID and App Secret parameters. + +>**Note:** +**You should not share/leak your App ID or App Secret.** ### 2. Requesting authorization -To request the authorization token, you should visit the `/oauth/authorize` endpoint. You can do that by visiting manually the URL: +To request the authorization code, you should redirect the user to the `/oauth/authorize` endpoint: + +``` +https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code&state=your_unique_state_hash +``` + +This will ask the user to approve the applications access to their account and then redirect back to the `REDIRECT_URI` you provided. + +The redirect will include the GET `code` parameter, for example: ``` -http://localhost:3000/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code +http://myapp.com/oauth/redirect?code=1234567890&state=your_unique_state_hash ``` -Where REDIRECT_URI is the URL in your app where users will be sent after authorization. +You should then use the `code` to request an access token. + +>**Important:** +It is highly recommended that you send a `state` value with the request to `/oauth/authorize` and +validate that value is returned and matches in the redirect request. +This is important to prevent [CSFR attacks](http://www.oauthsecurity.com/#user-content-authorization-code-flow), +`state` really should have been a requirement in the standard! ### 3. Requesting the access token -To request the access token, you should use the returned code and exchange it for an access token. To do that you can use any HTTP client. In this case, I used rest-client: +Once you have the authorization code you can request an `access_token` using the code, to do that you can use any HTTP client. In the following example, we are using Ruby's `rest-client`: ``` parameters = 'client_id=APP_ID&client_secret=APP_SECRET&code=RETURNED_CODE&grant_type=authorization_code&redirect_uri=REDIRECT_URI' @@ -46,6 +67,8 @@ RestClient.post 'http://localhost:3000/oauth/token', parameters "refresh_token": "8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1" } ``` +>**Note:** +The `redirect_uri` must match the `redirect_uri` used in the original authorization request. You can now make requests to the API with the access token returned. @@ -60,7 +83,7 @@ GET https://localhost:3000/api/v3/user?access_token=OAUTH-TOKEN Or you can put the token to the Authorization header: ``` -curl -H "Authorization: Bearer OAUTH-TOKEN" https://localhost:3000/api/v3/user +curl --header "Authorization: Bearer OAUTH-TOKEN" https://localhost:3000/api/v3/user ``` ## Resource Owner Password Credentials @@ -77,6 +100,9 @@ The credentials should only be used when there is a high degree of trust between client is part of the device operating system or a highly privileged application), and when other authorization grant types are not available (such as an authorization code). +>**Important:** +Never store the users credentials and only use this grant type when your client is deployed to a trusted environment, in 99% of cases [personal access tokens] are a better choice. + Even though this grant type requires direct client access to the resource owner credentials, the resource owner credentials are used for a single request and are exchanged for an access token. This grant type can eliminate the need for the client to store the resource owner credentials for future use, by exchanging the credentials with a long-lived access token or refresh token. diff --git a/doc/api/projects.md b/doc/api/projects.md index d065b01f706b863549ebdd975e7d7c9dfab2bd7c..3e89786210701295d4bc1ee78b15d7e4e354d1bc 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -533,7 +533,7 @@ POST /projects/:id/star | `id` | integer | yes | The ID of the project | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star" ``` Example response: @@ -599,7 +599,7 @@ DELETE /projects/:id/star | `id` | integer | yes | The ID of the project | ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star" +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star" ``` Example response: @@ -669,7 +669,7 @@ POST /projects/:id/archive | `id` | integer | yes | The ID of the project | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/archive" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/archive" ``` Example response: @@ -755,7 +755,7 @@ POST /projects/:id/unarchive | `id` | integer | yes | The ID of the project | ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/unarchive" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/unarchive" ``` Example response: @@ -862,95 +862,9 @@ Parameters: In Markdown contexts, the link is automatically expanded when the format in `markdown` is used. -## Team members +## Project members -### List project team members - -Get a list of a project's team members. - -``` -GET /projects/:id/members -``` - -Parameters: - -- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project -- `query` (optional) - Query string to search for members - -### Get project team member - -Gets a project team member. - -``` -GET /projects/:id/members/:user_id -``` - -Parameters: - -- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project -- `user_id` (required) - The ID of a user - -```json -{ - "id": 1, - "username": "john_smith", - "email": "john@example.com", - "name": "John Smith", - "state": "active", - "created_at": "2012-05-23T08:00:58Z", - "access_level": 40 -} -``` - -### Add project team member - -Adds a user to a project team. This is an idempotent method and can be called multiple times -with the same parameters. Adding team membership to a user that is already a member does not -affect the existing membership. - -``` -POST /projects/:id/members -``` - -Parameters: - -- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project -- `user_id` (required) - The ID of a user to add -- `access_level` (required) - Project access level - -### Edit project team member - -Updates a project team member to a specified access level. - -``` -PUT /projects/:id/members/:user_id -``` - -Parameters: - -- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project -- `user_id` (required) - The ID of a team member -- `access_level` (required) - Project access level - -### Remove project team member - -Removes a user from a project team. - -``` -DELETE /projects/:id/members/:user_id -``` - -Parameters: - -- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project -- `user_id` (required) - The ID of a team member - -This method removes the project member if the user has the proper access rights to do so. -It returns a status code 403 if the member does not have the proper rights to perform this action. -In all other cases this method is idempotent and revoking team membership for a user who is not -currently a team member is considered success. -Please note that the returned JSON currently differs slightly. Thus you should not -rely on the returned JSON structure. +Please consult the [Project Members](members.md) documentation. ### Share project with group diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md index 1b8ee88b4edf8086bedc5120efb1ad64d7c46b0b..fc3af5544de021765f451124ae26faf8593f309d 100644 --- a/doc/api/repository_files.md +++ b/doc/api/repository_files.md @@ -13,7 +13,7 @@ GET /projects/:id/repository/files ``` ```bash -curl -X GET -H '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/v3/projects/13083/repository/files?file_path=app/models/key.rb&ref=master' ``` Example response: @@ -44,7 +44,7 @@ POST /projects/:id/repository/files ``` ```bash -curl -X POST -H 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&content=some%20content&commit_message=create%20a%20new%20file' +curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&content=some%20content&commit_message=create%20a%20new%20file' ``` Example response: @@ -71,7 +71,7 @@ PUT /projects/:id/repository/files ``` ```bash -curl -X PUT -H 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&content=some%20other%20content&commit_message=update%20file' +curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&content=some%20other%20content&commit_message=update%20file' ``` Example response: @@ -107,7 +107,7 @@ DELETE /projects/:id/repository/files ``` ```bash -curl -X PUT -H 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&commit_message=delete%20file' +curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&commit_message=delete%20file' ``` Example response: diff --git a/doc/api/runners.md b/doc/api/runners.md index ddfa298f79d00870c67dc81e8f4691bcf34ac3f2..28610762dca875a5708030e2977b8bf1ae913b01 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 -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/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 -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/all" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/all" ``` Example response: @@ -108,7 +108,7 @@ GET /runners/:id | `id` | integer | yes | The ID of a runner | ``` -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/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 -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6" -F "description=test-1-20150125-test" -F "tag_list=ruby,mysql,tag1,tag2" +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" ``` Example response: @@ -207,7 +207,7 @@ DELETE /runners/:id | `id` | integer | yes | The ID of a runner | ``` -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6" +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6" ``` Example response: @@ -237,7 +237,7 @@ GET /projects/:id/runners | `id` | integer | yes | The ID of a project | ``` -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners" ``` Example response: @@ -275,7 +275,7 @@ POST /projects/:id/runners | `runner_id` | integer | yes | The ID of a runner | ``` -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners" -F "runner_id=9" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners" --form "runner_id=9" ``` Example response: @@ -306,7 +306,7 @@ DELETE /projects/:id/runners/:runner_id | `runner_id` | integer | yes | The ID of a runner | ``` -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners/9" +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners/9" ``` Example response: diff --git a/doc/api/session.md b/doc/api/session.md index 066a055702df222478f0996aeee6105ef81e6932..9076c48b899decf0a1e1fa54ceeb30c677f1d668 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 -X POST "https://gitlab.example.com/api/v3/session?login=john_smith&password=strongpassw0rd" +curl --request POST "https://gitlab.example.com/api/v3/session?login=john_smith&password=strongpassw0rd" ``` Example response: diff --git a/doc/api/settings.md b/doc/api/settings.md index ea39b32561c38048a89d6857e2c6ac654b7d68d7..a76dad0ebd47b9d2d1b1f3d15167d3c1f9321f96 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -13,7 +13,7 @@ GET /application/settings ``` ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings ``` Example response: @@ -75,7 +75,7 @@ PUT /application/settings | `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. ```bash -curl -X PUT -H "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/v3/application/settings?signup_enabled=false&default_project_visibility=1 ``` Example response: diff --git a/doc/api/sidekiq_metrics.md b/doc/api/sidekiq_metrics.md index ebd131c94ca960d4bfca513265e2b71466f503a9..1ae732d40d6cf955f5dedd1db8374f25fc1ef41a 100644 --- a/doc/api/sidekiq_metrics.md +++ b/doc/api/sidekiq_metrics.md @@ -15,7 +15,7 @@ GET /sidekiq/queue_metrics ``` ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/queue_metrics +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/queue_metrics ``` Example response: @@ -40,7 +40,7 @@ GET /sidekiq/process_metrics ``` ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/process_metrics +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/process_metrics ``` Example response: @@ -82,7 +82,7 @@ GET /sidekiq/job_stats ``` ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/job_stats +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/job_stats ``` Example response: @@ -106,7 +106,7 @@ GET /sidekiq/compound_metrics ``` ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/compound_metrics +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/compound_metrics ``` Example response: diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md index dc036d7e27fce417c1cc27dab18f620e146cfee6..1802fae14feb8c87782b4eaac7a2c5c6607c3d99 100644 --- a/doc/api/system_hooks.md +++ b/doc/api/system_hooks.md @@ -20,7 +20,7 @@ GET /hooks Example request: ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks ``` Example response: @@ -52,7 +52,7 @@ POST /hooks Example request: ```bash -curl -X POST -H "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/v3/hooks?url=https://gitlab.example.com/hook" ``` Example response: @@ -80,7 +80,7 @@ GET /hooks/:id Example request: ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2 +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2 ``` Example response: @@ -117,7 +117,7 @@ DELETE /hooks/:id Example request: ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2 +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2 ``` Example response: diff --git a/doc/api/tags.md b/doc/api/tags.md index ac9fac92f4cc24b30e7e3001269d10e90fb0487c..5405911745653f6d0ebe3edfe8231158f87d3dad 100644 --- a/doc/api/tags.md +++ b/doc/api/tags.md @@ -56,7 +56,7 @@ Parameters: | `tag_name` | string | yes | The name of the tag | ```bash -curl -H "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/v3/projects/5/repository/tags/v1.0.0 ``` Example Response: diff --git a/doc/api/todos.md b/doc/api/todos.md index c9e1e83e28a354cf292adcbbd5f1b72102cd6eb2..0cd644dfd2fe2bbd4e9e84cab7b990fa2e1fecda 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 -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos ``` Example Response: @@ -194,7 +194,7 @@ Parameters: | `id` | integer | yes | The ID of a todo | ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos/130 +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos/130 ``` Example Response: @@ -284,7 +284,7 @@ DELETE /todos ``` ```bash -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos ``` Example Response: diff --git a/doc/ci/examples/php.md b/doc/ci/examples/php.md index bfafcc44d66ef9e55822d97a71a991cbce97fe2f..175e9d79904e19d08f89c10babd9d0d515f1e1d0 100644 --- a/doc/ci/examples/php.md +++ b/doc/ci/examples/php.md @@ -49,7 +49,7 @@ apt-get update -yqq apt-get install git -yqq # Install phpunit, the tool that we will use for testing -curl -Lo /usr/local/bin/phpunit https://phar.phpunit.de/phpunit.phar +curl --location --output /usr/local/bin/phpunit https://phar.phpunit.de/phpunit.phar chmod +x /usr/local/bin/phpunit # Install mysql driver @@ -235,7 +235,7 @@ cache: before_script: # Install composer dependencies -- curl -sS https://getcomposer.org/installer | php +- curl --silent --show-error https://getcomposer.org/installer | php - php composer.phar install ... diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md index 57a12526363b09aa98a940d14f9d7b779b247ac6..6c6767fea0b14ec04031e1edb0414226aa58ca9c 100644 --- a/doc/ci/triggers/README.md +++ b/doc/ci/triggers/README.md @@ -77,9 +77,9 @@ See the [Examples](#examples) section below for more details. Using cURL you can trigger a rebuild with minimal effort, for example: ```bash -curl -X POST \ - -F token=TOKEN \ - -F ref=master \ +curl --request POST \ + --form token=TOKEN \ + --form ref=master \ https://gitlab.example.com/api/v3/projects/9/trigger/builds ``` @@ -88,7 +88,7 @@ In this case, the project with ID `9` will get rebuilt on `master` branch. Alternatively, you can pass the `token` and `ref` arguments in the query string: ```bash -curl -X POST \ +curl --request POST \ "https://gitlab.example.com/api/v3/projects/9/trigger/builds?token=TOKEN&ref=master" ``` @@ -103,7 +103,7 @@ need to add in project's A `.gitlab-ci.yml`: build_docs: stage: deploy script: - - "curl -X POST -F token=TOKEN -F 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/v3/projects/9/trigger/builds" only: - tags ``` @@ -158,10 +158,10 @@ You can then trigger a rebuild while you pass the `UPLOAD_TO_S3` variable and the script of the `upload_package` job will run: ```bash -curl -X POST \ - -F token=TOKEN \ - -F ref=master \ - -F "variables[UPLOAD_TO_S3]=true" \ +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 ``` @@ -172,7 +172,7 @@ in conjunction with cron. The example below triggers a build on the `master` branch of project with ID `9` every night at `00:30`: ```bash -30 0 * * * curl -X POST -F token=TOKEN -F 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/v3/projects/9/trigger/builds ``` [ci-229]: https://gitlab.com/gitlab-org/gitlab-ci/merge_requests/229 diff --git a/doc/development/README.md b/doc/development/README.md index 7b5f7ff8ad3f05cc61ff0010ad046fd7fb7e4cdf..bf67b5d8dff0ee07897e42ff2a08c03092217c6b 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -4,18 +4,17 @@ - [CONTRIBUTING.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md) main contributing guide - [PROCESS.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/PROCESS.md) contributing process -- [GitLab Development Kit (GDK)](https://gitlab.com/gitlab-org/gitlab-development-kit) to install a development version +- [GitLab Development Kit (GDK)](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/README.md) to install a development version ## Styleguides -- [Documentation styleguide](development/doc_styleguide.md) Use this styleguide if you are +- [Documentation styleguide](doc_styleguide.md) Use this styleguide if you are contributing to documentation. -- [Migration Style Guide](migration_style_guide.md) for creating safe migrations +- [SQL Migration Style Guide](migration_style_guide.md) for creating safe SQL migrations - [Testing standards and style guidelines](testing.md) -- [UI guide](ui_guide.md) for building GitLab with existing css styles and elements +- [UI guide](ui_guide.md) for building GitLab with existing CSS styles and elements - [SQL guidelines](sql.md) for SQL guidelines - ## Process - [Code review guidelines](code_review.md) for reviewing code and having code reviewed. diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md index 994005f929f2adbcba819206c0366916ab78a6b1..927a18724135411c679050b6a36f7cba28004b12 100644 --- a/doc/development/doc_styleguide.md +++ b/doc/development/doc_styleguide.md @@ -355,7 +355,7 @@ Below is a set of [cURL][] examples that you can use in the API documentation. Get the details of a group: ```bash -curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/gitlab-org +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/gitlab-org ``` #### cURL example with parameters passed in the URL @@ -363,7 +363,7 @@ curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/ Create a new project under the authenticated user's namespace: ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects?name=foo" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects?name=foo" ``` #### Post data using cURL's --data @@ -373,7 +373,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" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects" +curl --data "name=foo" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects" ``` #### Post data using JSON content @@ -382,7 +382,7 @@ curl --data "name=foo" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab. and double quotes. ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -H "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/v3/groups ``` #### Post data using form-data @@ -391,7 +391,7 @@ Instead of using JSON or urlencode you can use multipart/form-data which properly handles data encoding: ```bash -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -F "title=ssh-key" -F "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/v3/users/25/keys ``` The above example is run by and administrator and will add an SSH public key @@ -405,7 +405,7 @@ contains spaces in its title. Observe how spaces are escaped using the `%20` ASCII code. ```bash -curl -X POST -H "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/v3/projects/42/issues?title=Hello%20Dude" ``` Use `%2F` for slashes (`/`). @@ -417,7 +417,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 -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -d "domain_whitelist[]=*.example.com" -d "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/v3/application/settings ``` [cURL]: http://curl.haxx.se/ "cURL website" diff --git a/doc/development/newlines_styleguide.md b/doc/development/newlines_styleguide.md new file mode 100644 index 0000000000000000000000000000000000000000..e03adcaadea5d38ca247b9025bcdba0283ee0405 --- /dev/null +++ b/doc/development/newlines_styleguide.md @@ -0,0 +1,102 @@ +# Newlines styleguide + +This style guide recommends best practices for newlines in Ruby code. + +## Rule: separate code with newlines only when it makes sense from logic perspectice + +```ruby +# bad +def method + issue = Issue.new + + issue.save + + render json: issue +end +``` + +```ruby +# good +def method + issue = Issue.new + issue.save + + render json: issue +end +``` + +## Rule: separate code and block with newlines + +### Newline before block + +```ruby +# bad +def method + issue = Issue.new + if issue.save + render json: issue + end +end +``` + +```ruby +# good +def method + issue = Issue.new + + if issue.save + render json: issue + end +end +``` + +## Newline after block + +```ruby +# bad +def method + if issue.save + issue.send_email + end + render json: issue +end +``` + +```ruby +# good +def method + if issue.save + issue.send_email + end + + render json: issue +end +``` + +### Exception: no need for newline when code block starts or ends right inside another code block + +```ruby +# bad +def method + + if issue + + if issue.valid? + issue.save + end + + end + +end +``` + +```ruby +# good +def method + if issue + if issue.valid? + issue.save + end + end +end +``` diff --git a/doc/development/performance.md b/doc/development/performance.md index fb37b3a889c73279a615decd94302749d3abc40f..7ff603e2c4a91aff658cf9acf1c71c052eae692e 100644 --- a/doc/development/performance.md +++ b/doc/development/performance.md @@ -15,8 +15,8 @@ The process of solving performance problems is roughly as follows: 3. Add your findings based on the measurement period (screenshots of graphs, timings, etc) to the issue mentioned in step 1. 4. Solve the problem. -5. Create a merge request, assign the "performance" label and ping the right - people (e.g. [@yorickpeterse][yorickpeterse] and [@joshfng][joshfng]). +5. Create a merge request, assign the "Performance" label and assign it to + [@yorickpeterse][yorickpeterse] for reviewing. 6. Once a change has been deployed make sure to _again_ measure for at least 24 hours to see if your changes have any impact on the production environment. 7. Repeat until you're done. @@ -36,8 +36,8 @@ graphs/dashboards. GitLab provides two built-in tools to aid the process of improving performance: -* [Sherlock](doc/development/profiling.md#sherlock) -* [GitLab Performance Monitoring](doc/monitoring/performance/monitoring.md) +* [Sherlock](profiling.md#sherlock) +* [GitLab Performance Monitoring](../monitoring/performance/monitoring.md) GitLab employees can use GitLab.com's performance monitoring systems located at , this requires you to log in using your @@ -254,5 +254,4 @@ referencing an object directly may even slow code down. [#15607]: https://gitlab.com/gitlab-org/gitlab-ce/issues/15607 [yorickpeterse]: https://gitlab.com/u/yorickpeterse -[joshfng]: https://gitlab.com/u/joshfng [anti-pattern]: https://en.wikipedia.org/wiki/Anti-pattern diff --git a/doc/development/ui_guide.md b/doc/development/ui_guide.md index 6525228801990c6bca46d8e6f95da0d0c6d352a6..3a8c823e0260205573449e7133f9f73613167dac 100644 --- a/doc/development/ui_guide.md +++ b/doc/development/ui_guide.md @@ -47,6 +47,42 @@ information from database or file system * `rss` for rss/atom feed * `plus` for link or dropdown that lead to page where you create new object (For example new issue page) +### SVGs + +When exporting SVGs, be sure to follow the following guidelines: + +1. Convert all strokes to outlines. +2. Use pathfinder tools to combine overlapping paths and create compound paths. +3. SVGs that are limited to one color should be exported without a fill color so the color can be set using CSS. +4. Ensure that exported SVGs have been run through an [SVG cleaner](https://github.com/RazrFalcon/SVGCleaner) to remove unused elements and attributes. + +You can open your svg in a text editor to ensure that it is clean. +Incorrect files will look like this: + +```xml + + + + Group + Created with Sketch. + + + + + + + + + + +``` + +Correct file will look like this: + +```xml + +``` + ## Buttons diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md index abd693cf72dd701df307c933a39d6355f7f3ba55..2574c2c04727c58a76bca7a86d1a04ef2ceff035 100644 --- a/doc/development/what_requires_downtime.md +++ b/doc/development/what_requires_downtime.md @@ -31,6 +31,14 @@ operation, even when using `ALGORITHM=INPLACE` and `LOCK=NONE`. This means downtime _may_ be required when modifying large tables as otherwise the operation could potentially take hours to complete. +Adding a column with a default value _can_ be done without requiring downtime +when using the migration helper method +`Gitlab::Database::MigrationHelpers#add_column_with_default`. This method works +similar to `add_column` except it updates existing rows in batches without +blocking access to the table being modified. See ["Adding Columns With Default +Values"](migration_style_guide.html#adding-columns-with-default-values) for more +information on how to use this method. + ## Dropping Columns On PostgreSQL you can safely remove an existing column without the need for diff --git a/doc/install/installation.md b/doc/install/installation.md index c670913e4f32e1853e2b39fdd1f3cbc95d5cef07..ea8fbe1fa6abd2604526c4b7f6daa3095c1c7ab1 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -89,7 +89,7 @@ Is the system packaged Git too old? Remove it and compile from source. # Download and compile from source cd /tmp - curl -O --progress https://www.kernel.org/pub/software/scm/git/git-2.7.4.tar.gz + curl --remote-name --progress https://www.kernel.org/pub/software/scm/git/git-2.7.4.tar.gz echo '7104c4f5d948a75b499a954524cb281fe30c6649d8abe20982936f75ec1f275b git-2.7.4.tar.gz' | shasum -a256 -c - && tar -xzf git-2.7.4.tar.gz cd git-2.7.4/ ./configure @@ -108,8 +108,7 @@ Then select 'Internet Site' and press enter to confirm the hostname. ## 2. Ruby -_**Note:** The current supported Ruby version is 2.1.x. Ruby 2.2 and 2.3 are -currently not supported._ +_**Note:** The current supported Ruby versions are 2.1.x and 2.3.x. 2.3.x is preferred, and support for 2.1.x will be dropped in the future. The use of Ruby version managers such as [RVM], [rbenv] or [chruby] with GitLab in production, frequently leads to hard to diagnose problems. For example, @@ -124,9 +123,9 @@ Remove the old Ruby 1.8 if present: Download Ruby and compile it: mkdir /tmp/ruby && cd /tmp/ruby - curl -O --progress https://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.8.tar.gz - echo 'c7e50159357afd87b13dc5eaf4ac486a70011149 ruby-2.1.8.tar.gz' | shasum -c - && tar xzf ruby-2.1.8.tar.gz - cd ruby-2.1.8 + curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.1.tar.gz + echo 'c39b4001f7acb4e334cb60a0f4df72d434bef711 ruby-2.3.1.tar.gz' | shasum -c - && tar xzf ruby-2.3.1.tar.gz + cd ruby-2.3.1 ./configure --disable-install-rdoc make sudo make install @@ -143,7 +142,7 @@ gitlab-workhorse we need a Go compiler. The instructions below assume you use 64-bit Linux. You can find downloads for other platforms at the [Go download page](https://golang.org/dl). - curl -O --progress https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz + curl --remote-name --progress https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz echo '43afe0c5017e502630b1aea4d44b8a7f059bf60d7f29dfd58db454d4e4e0ae53 go1.5.3.linux-amd64.tar.gz' | shasum -a256 -c - && \ sudo tar -C /usr/local -xzf go1.5.3.linux-amd64.tar.gz sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ @@ -595,15 +594,17 @@ for the changes to take effect. ### Custom Redis Connection -If you'd like Resque to connect to a Redis server on a non-standard port or on a different host, you can configure its connection string via the `config/resque.yml` file. +If you'd like to connect to a Redis server on a non-standard port or on a different host, you can configure its connection string via the `config/resque.yml` file. # example - production: redis://redis.example.tld:6379 + production: + url: redis://redis.example.tld:6379 If you want to connect the Redis server via socket, then use the "unix:" URL scheme and the path to the Redis socket file in the `config/resque.yml` file. # example - production: unix:/path/to/redis/socket + production: + url: unix:/path/to/redis/socket ### Custom SSH Connection diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md index 63432b044323a1d7d8f430d631baf71e165fe3ca..2eb6266ebe7952e8235ef09273711b343e567a3f 100644 --- a/doc/integration/bitbucket.md +++ b/doc/integration/bitbucket.md @@ -14,7 +14,7 @@ Bitbucket will generate an application ID and secret key for you to use. 1. Select "Add consumer". 1. Provide the required details. - - Name: This can be anything. Consider something like "\'s GitLab" or "\'s GitLab" or something else descriptive. + - Name: This can be anything. Consider something like `'s GitLab` or `'s GitLab` or something else descriptive. - Application description: Fill this in if you wish. - URL: The URL to your GitLab installation. 'https://gitlab.company.com' 1. Select "Save". diff --git a/doc/integration/github.md b/doc/integration/github.md index 340c8a55fb3a50b9924b25f12243cd8d12ce5b73..8a01afd1177931b7d140d03ed5d73f13728876af 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -16,7 +16,7 @@ GitHub will generate an application ID and secret key for you to use. 1. Select "Register new application". 1. Provide the required details. - - Application name: This can be anything. Consider something like "\'s GitLab" or "\'s GitLab" or something else descriptive. + - Application name: This can be anything. Consider something like `'s GitLab` or `'s GitLab` or something else descriptive. - Homepage URL: The URL to your GitLab installation. 'https://gitlab.company.com' - Application description: Fill this in if you wish. - Authorization callback URL is 'http(s)://${YOUR_DOMAIN}' diff --git a/doc/integration/gitlab.md b/doc/integration/gitlab.md index b215cc7c609a7cfc4473f9f1368106b9dec36f5f..6d8f3912ede101f0bcfbd7ef00734d7e7473cde8 100644 --- a/doc/integration/gitlab.md +++ b/doc/integration/gitlab.md @@ -14,7 +14,7 @@ GitLab.com will generate an application ID and secret key for you to use. 1. Select "New application". 1. Provide the required details. - - Name: This can be anything. Consider something like "\'s GitLab" or "\'s GitLab" or something else descriptive. + - Name: This can be anything. Consider something like `'s GitLab` or `'s GitLab` or something else descriptive. - Redirect URI: ``` diff --git a/doc/integration/twitter.md b/doc/integration/twitter.md index 4769f26b259ff5fdb668156f6ad8b96c85a2ba2c..abbea09f22fcc8bc7d0274851ed5c0c45ec6e84c 100644 --- a/doc/integration/twitter.md +++ b/doc/integration/twitter.md @@ -7,7 +7,7 @@ To enable the Twitter OmniAuth provider you must register your application with 1. Select "Create new app" 1. Fill in the application details. - - Name: This can be anything. Consider something like "\'s GitLab" or "\'s GitLab" or + - Name: This can be anything. Consider something like `'s GitLab` or `'s GitLab` or something else descriptive. - Description: Create a description. - Website: The URL to your GitLab installation. 'https://gitlab.example.com' diff --git a/doc/legal/corporate_contributor_license_agreement.md b/doc/legal/corporate_contributor_license_agreement.md index 7b94506c29785b55d27a9e2c1ea40fa3ed3779ae..edd6c59138fd89dcebeb31abbd817731402ad29a 100644 --- a/doc/legal/corporate_contributor_license_agreement.md +++ b/doc/legal/corporate_contributor_license_agreement.md @@ -6,13 +6,17 @@ You accept and agree to the following terms and conditions for Your present and "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with GitLab B.V.. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - "Contribution" shall mean the code, documentation or other original works of authorship expressly identified in Schedule B, as well as any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to GitLab B.V. for inclusion in, or documentation of, any of the products owned or managed by GitLab B.V. (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to GitLab B.V. or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, GitLab B.V. for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." + "Contribution" shall mean the code, documentation or other original works of authorship, including any modifications or additions to an existing work, that is submitted by You to GitLab B.V. for inclusion in, or documentation of, any of the products owned or managed by GitLab B.V. (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to GitLab B.V. or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, GitLab B.V. for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." -2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. +2. Grant of Copyright License. -3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. +Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. -4. You represent that You are legally entitled to grant the above license. You represent further that each employee of the Corporation designated on Schedule A below (or in a subsequent written modification to that Schedule) is authorized to submit Contributions on behalf of the Corporation. +3. Grant of Patent License. + +Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. + +4. You represent that You are legally entitled to grant the above license. You represent further that each employee of the Corporation is authorized to submit Contributions on behalf of the Corporation, but excluding employees that are designated in writing by You as "Not authorized to submit Contributions on behalf of [name of corporation here]." 5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). @@ -20,6 +24,6 @@ You accept and agree to the following terms and conditions for Your present and 7. Should You wish to submit work that is not Your original creation, You may submit it to GitLab B.V. separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]". -8. It is your responsibility to notify GitLab B.V. when any change is required to the list of designated employees authorized to submit Contributions on behalf of the Corporation, or to the Corporation's Point of Contact with GitLab B.V.. +8. It is your responsibility to notify GitLab B.V. when any change is required to the designation of employees not authorized to submit Contributions on behalf of the Corporation, or to the Corporation's Point of Contact with GitLab B.V.. This text is licensed under the [Creative Commons Attribution 3.0 License](https://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office. diff --git a/doc/monitoring/health_check.md b/doc/monitoring/health_check.md index 70326f1ff80da5444a1fb912c0f993019c54ab5a..eac57bc3de4b9da3e95b57c846f2a0bb85694139 100644 --- a/doc/monitoring/health_check.md +++ b/doc/monitoring/health_check.md @@ -24,7 +24,7 @@ https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN or as an HTTP header: ```bash -curl -H "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json +curl --header "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json ``` ## Using the Endpoint @@ -45,7 +45,7 @@ You can also ask for the status of specific services: For example, the JSON output of the following health check: ```bash -curl -H "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json +curl --header "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json ``` would be like: diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 52e68692d45cf56cb2e76535f5fbd1b91b1d1bc4..8d0fa432b1d25273d65a3890d5a4087c7a6872ef 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -11,12 +11,13 @@ You can only restore a backup to exactly the same version of GitLab that you cre on, for example 7.2.1. The best way to migrate your repositories from one server to another is through backup restore. -You need to keep a separate copy of `/etc/gitlab/gitlab-secrets.json` -(for omnibus packages) or `/home/git/gitlab/.secret` (for installations -from source). This file contains the database encryption key used -for two-factor authentication. If you restore a GitLab backup without -restoring the database encryption key, users who have two-factor -authentication enabled will lose access to your GitLab server. +You need to keep separate copies of `/etc/gitlab/gitlab-secrets.json` and +`/etc/gitlab/gitlab.rb` (for omnibus packages) or +`/home/git/gitlab/config/secrets.yml` (for installations from source). This file +contains the database encryption keys used for two-factor authentication and CI +secret variables, among other things. If you restore a GitLab backup without +restoring the database encryption key, users who have two-factor authentication +enabled will lose access to your GitLab server. ``` # use this command if you've installed GitLab with the Omnibus package @@ -235,11 +236,12 @@ of using encryption in the first place! If you use an Omnibus package please see the [instructions in the readme to backup your configuration](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#backup-and-restore-omnibus-gitlab-configuration). If you have a cookbook installation there should be a copy of your configuration in Chef. -If you have an installation from source, please consider backing up your `.secret` file, `gitlab.yml` file, any SSL keys and certificates, and your [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079). +If you have an installation from source, please consider backing up your `config/secrets.yml` file, `gitlab.yml` file, any SSL keys and certificates, and your [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079). -At the very **minimum** you should backup `/etc/gitlab/gitlab-secrets.json` -(Omnibus) or `/home/git/gitlab/.secret` (source) to preserve your -database encryption key. +At the very **minimum** you should backup `/etc/gitlab/gitlab.rb` and +`/etc/gitlab/gitlab-secrets.json` (Omnibus), or +`/home/git/gitlab/config/secrets.yml` (source) to preserve your database +encryption key. ## Restore a previously created backup @@ -254,11 +256,11 @@ the SQL database it needs to import data into ('gitlabhq_production'). All existing data will be either erased (SQL) or moved to a separate directory (repositories, uploads). -If some or all of your GitLab users are using two-factor authentication -(2FA) then you must also make sure to restore -`/etc/gitlab/gitlab-secrets.json` (Omnibus) or `/home/git/gitlab/.secret` -(installations from source). Note that you need to run `gitlab-ctl -reconfigure` after changing `gitlab-secrets.json`. +If some or all of your GitLab users are using two-factor authentication (2FA) +then you must also make sure to restore `/etc/gitlab/gitlab.rb` and +`/etc/gitlab/gitlab-secrets.json` (Omnibus), or +`/home/git/gitlab/config/secrets.yml` (installations from source). Note that you +need to run `gitlab-ctl reconfigure` after changing `gitlab-secrets.json`. ### Installation from source diff --git a/doc/raketasks/user_management.md b/doc/raketasks/user_management.md index 629d38efc5355042124c94a0a9c88ba73a7a32aa..8a5e2d6e16bfe94e4d194dc421411ce10e2a9dfc 100644 --- a/doc/raketasks/user_management.md +++ b/doc/raketasks/user_management.md @@ -60,8 +60,8 @@ block_auto_created_users: false ## Disable Two-factor Authentication (2FA) for all users This task will disable 2FA for all users that have it enabled. This can be -useful if GitLab's `.secret` file has been lost and users are unable to login, -for example. +useful if GitLab's `config/secrets.yml` file has been lost and users are unable +to login, for example. ```bash # omnibus-gitlab diff --git a/doc/update/4.0-to-4.1.md b/doc/update/4.0-to-4.1.md index c163bfd348d285b0d33142d9185ca17ef00cbe42..c66c6dd0fd8d9a0172f409593b5201fb93a485f3 100644 --- a/doc/update/4.0-to-4.1.md +++ b/doc/update/4.0-to-4.1.md @@ -42,7 +42,7 @@ sudo -u gitlab -H bundle exec rake db:migrate RAILS_ENV=production sudo mv /etc/init.d/gitlab /etc/init.d/gitlab.old # get new one using sidekiq -sudo curl -L --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlab-recipes/4-1-stable/init.d/gitlab +sudo curl --location --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlab-recipes/4-1-stable/init.d/gitlab sudo chmod +x /etc/init.d/gitlab ``` diff --git a/doc/update/4.2-to-5.0.md b/doc/update/4.2-to-5.0.md index ee6de51c9233207055f19bdd0f044b419f173a99..7654f4a0131d1348b2a710c9870eb41121dc0822 100644 --- a/doc/update/4.2-to-5.0.md +++ b/doc/update/4.2-to-5.0.md @@ -126,7 +126,7 @@ sudo chmod -R u+rwX /home/git/gitlab/tmp/pids ```bash # init.d sudo rm /etc/init.d/gitlab -sudo curl -L --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlab-recipes/5-0-stable/init.d/gitlab +sudo curl --location --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlab-recipes/5-0-stable/init.d/gitlab sudo chmod +x /etc/init.d/gitlab # unicorn diff --git a/doc/update/5.0-to-5.1.md b/doc/update/5.0-to-5.1.md index f0fddcf83afd9e9e1d7edde97c4f74eccfed30f7..c19a819ab5a3f88826b850887fd0dcc5aecf4639 100644 --- a/doc/update/5.0-to-5.1.md +++ b/doc/update/5.0-to-5.1.md @@ -63,7 +63,7 @@ sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production ```bash # init.d sudo rm /etc/init.d/gitlab -sudo curl -L --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlab-recipes/5-1-stable/init.d/gitlab +sudo curl --location --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlab-recipes/5-1-stable/init.d/gitlab sudo chmod +x /etc/init.d/gitlab ``` diff --git a/doc/update/5.2-to-5.3.md b/doc/update/5.2-to-5.3.md index c5254f6fb0c73fb0e94b91c495b5965bf13f676e..fe8990b6843ccfaeed4da7a146b78db8940a3f7f 100644 --- a/doc/update/5.2-to-5.3.md +++ b/doc/update/5.2-to-5.3.md @@ -67,7 +67,7 @@ sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production ```bash sudo rm /etc/init.d/gitlab -sudo curl -L --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlabhq/5-3-stable/lib/support/init.d/gitlab +sudo curl --location --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlabhq/5-3-stable/lib/support/init.d/gitlab sudo chmod +x /etc/init.d/gitlab ``` diff --git a/doc/update/5.3-to-5.4.md b/doc/update/5.3-to-5.4.md index c4a6146dcda3cbd7b4d4de6d1e1f4f36f22fe424..5f82ad7d444723d9a95d180a8d88b7f2acc867b2 100644 --- a/doc/update/5.3-to-5.4.md +++ b/doc/update/5.3-to-5.4.md @@ -71,7 +71,7 @@ sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production ```bash sudo rm /etc/init.d/gitlab -sudo curl -L --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlabhq/5-4-stable/lib/support/init.d/gitlab +sudo curl --location --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlabhq/5-4-stable/lib/support/init.d/gitlab sudo chmod +x /etc/init.d/gitlab ``` diff --git a/doc/update/6.9-to-7.0.md b/doc/update/6.9-to-7.0.md index 236430b5951618062a534e88e9a20ce9dfd2e5cc..5352fd52f93ac287a0c63e49509154343c4cedc6 100644 --- a/doc/update/6.9-to-7.0.md +++ b/doc/update/6.9-to-7.0.md @@ -33,7 +33,7 @@ Download and compile Ruby: ```bash mkdir /tmp/ruby && cd /tmp/ruby -curl -L --progress ftp://ftp.ruby-lang.org/pub/ruby/2.1/ruby-2.1.2.tar.gz | tar xz +curl --location --progress ftp://ftp.ruby-lang.org/pub/ruby/2.1/ruby-2.1.2.tar.gz | tar xz cd ruby-2.1.2 ./configure --disable-install-rdoc make diff --git a/doc/update/7.0-to-7.1.md b/doc/update/7.0-to-7.1.md index a4e9be9946e07fdcbc7f1e7f9416607862a45236..71f39c44077e34443663f2bf31ab4f94cae90c87 100644 --- a/doc/update/7.0-to-7.1.md +++ b/doc/update/7.0-to-7.1.md @@ -33,7 +33,7 @@ Download and compile Ruby: ```bash mkdir /tmp/ruby && cd /tmp/ruby -curl -L --progress ftp://ftp.ruby-lang.org/pub/ruby/2.1/ruby-2.1.2.tar.gz | tar xz +curl --location --progress ftp://ftp.ruby-lang.org/pub/ruby/2.1/ruby-2.1.2.tar.gz | tar xz cd ruby-2.1.2 ./configure --disable-install-rdoc make diff --git a/doc/update/7.14-to-8.0.md b/doc/update/7.14-to-8.0.md index 305017b704816cea41a22b80e18efbd1d355ae7a..117e2afaaa0b946d67bcc269ef0b44b07aad5ed3 100644 --- a/doc/update/7.14-to-8.0.md +++ b/doc/update/7.14-to-8.0.md @@ -71,7 +71,7 @@ sudo -u git -H git checkout v2.6.5 First we download Go 1.5 and install it into `/usr/local/go`: ```bash -curl -O --progress https://storage.googleapis.com/golang/go1.5.linux-amd64.tar.gz +curl --remote-name --progress https://storage.googleapis.com/golang/go1.5.linux-amd64.tar.gz echo '5817fa4b2252afdb02e11e8b9dc1d9173ef3bd5a go1.5.linux-amd64.tar.gz' | shasum -c - && \ sudo tar -C /usr/local -xzf go1.5.linux-amd64.tar.gz sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md index 25343d484bae3cbc31bf214367f57e0c5d6f8be4..84c624cbcb7006c94d7249e9007710cb37fc9d34 100644 --- a/doc/update/8.10-to-8.11.md +++ b/doc/update/8.10-to-8.11.md @@ -46,7 +46,7 @@ sudo -u git -H git checkout 8-11-stable-ee ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch --all --tags -sudo -u git -H git checkout v3.2.1 +sudo -u git -H git checkout v3.3.3 ``` ### 5. Update gitlab-workhorse diff --git a/doc/user/admin_area/img/admin_labels.png b/doc/user/admin_area/img/admin_labels.png new file mode 100644 index 0000000000000000000000000000000000000000..1ee33a534abe4aca634869dea9330243ab3521f8 Binary files /dev/null and b/doc/user/admin_area/img/admin_labels.png differ diff --git a/doc/user/admin_area/labels.md b/doc/user/admin_area/labels.md new file mode 100644 index 0000000000000000000000000000000000000000..9e2a89ebdf600f4ed3130a9667f988316286f629 --- /dev/null +++ b/doc/user/admin_area/labels.md @@ -0,0 +1,9 @@ +# Labels + +## Default Labels + +### Define your own default Label Set + +Labels that are created within the Labels view on the Admin Dashboard will be automatically added to each new project. + +![Default label set](img/admin_labels.png) diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md index 1259a16330bc614afa98a645b491c3944a734bd2..0f7e9eede19ab54a785ce62cb519a06385dd9b57 100644 --- a/doc/user/project/labels.md +++ b/doc/user/project/labels.md @@ -22,26 +22,38 @@ created yet. ![Generate new labels](img/labels_generate.png) +Creating a new label from scratch is as easy as pressing the **New label** +button. From there on you can choose the name, give it an optional description, +a color and you are set. + +When you are ready press the **Create label** button to create the new label. + +![New label](img/labels_new_label.png) + --- -You can skip that and create a new label or click that link and GitLab will -generate a set of predefined labels for you. There 8 default generated labels +## Default Labels + +It's possible to populate the labels for your project from a set of predefined labels. + +### Generate GitLab's predefined label set + +![Generate new labels](img/labels_generate.png) + +Click the link to 'Generate a default set of labels' and GitLab will +generate a set of predefined labels for you. There are 8 default generated labels in total and you can see them in the screenshot below. ![Default generated labels](img/labels_default.png) --- -You can see that from the labels page you can have an overview of the number of -issues and merge requests assigned to each label. - -Creating a new label from scratch is as easy as pressing the **New label** -button. From there on you can choose the name, give it an optional description, -a color and you are set. +## Labels Overview -When you are ready press the **Create label** button to create the new label. +![Default generated labels](img/labels_default.png) -![New label](img/labels_new_label.png) +You can see that from the labels page you can have an overview of the number of +issues and merge requests assigned to each label. ## Prioritize labels diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md index e153471fa68b58c72398933dafa2c1cf257ccae9..fd8e9c65288774d9846ab81a3beb307be8bb1746 100644 --- a/doc/workflow/importing/import_projects_from_github.md +++ b/doc/workflow/importing/import_projects_from_github.md @@ -18,9 +18,6 @@ At its current state, GitHub importer can import: With GitLab 8.7+, references to pull requests and issues are preserved. -It is not yet possible to import your cross-repository pull requests (those from -forks). We are working on improving this in the near future. - The importer page is visible when you [create a new project][new-project]. Click on the **GitHub** link and, if you are logged in via the GitHub integration, you will be redirected to GitHub for permission to access your diff --git a/doc/workflow/shortcuts.md b/doc/workflow/shortcuts.md index ffcb832cdd7999fe6b99b851f34918181dfad8ae..36516883ef6234f76a82d1dd4ff31153dc14fa59 100644 --- a/doc/workflow/shortcuts.md +++ b/doc/workflow/shortcuts.md @@ -2,4 +2,75 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?' -![Shortcuts](shortcuts.png) \ No newline at end of file +## Global Shortcuts + +| Keyboard Shortcut | Description | +| ----------------- | ----------- | +| s | Focus search | +| ? | Show/hide this dialog | +| + shift + p | Toggle markdown preview | +| | Edit last comment (when focused on an empty textarea) | + +## Project Files Browsing + +| Keyboard Shortcut | Description | +| ----------------- | ----------- | +| | Move selection up | +| | Move selection down | +| enter | Open selection | + +## Finding Project File + +| Keyboard Shortcut | Description | +| ----------------- | ----------- | +| | Move selection up | +| | Move selection down | +| enter | Open selection | +| esc | Go back | + +## Global Dashboard + +| Keyboard Shortcut | Description | +| ----------------- | ----------- | +| g + a | Go to the activity feed | +| g + p | Go to projects | +| g + i | Go to issues | +| g + m | Go to merge requests | + +## Project + +| Keyboard Shortcut | Description | +| ----------------- | ----------- | +| g + p | Go to the project's home page | +| g + e | Go to the project's activity feed | +| g + f | Go to files | +| g + c | Go to commits | +| g + b | Go to builds | +| g + n | Go to network graph | +| g + g | Go to graphs | +| g + i | Go to issues | +| g + m | Go to merge requests | +| g + s | Go to snippets | +| t | Go to finding file | +| i | New issue | + +## Network Graph + +| Keyboard Shortcut | Description | +| ----------------- | ----------- | +| or h | Scroll left | +| or l | Scroll right | +| or k | Scroll up | +| or j | Scroll down | +| shift + or shift + k | Scroll to top | +| shift + or shift + j | Scroll to bottom | + +## Issues and Merge Requests + +| Keyboard Shortcut | Description | +| ----------------- | ----------- | +| a | Change assignee | +| m | Change milestone | +| r | Reply (quoting selected text) | +| e | Edit issue/merge request | +| l | Change label | \ No newline at end of file diff --git a/doc/workflow/shortcuts.png b/doc/workflow/shortcuts.png deleted file mode 100644 index a9b1c4b4dccf119a33781fd5e839e3dc4d6a808f..0000000000000000000000000000000000000000 Binary files a/doc/workflow/shortcuts.png and /dev/null differ diff --git a/features/explore/groups.feature b/features/explore/groups.feature index 5fc9b1356010c2433d626b51e83b203656e367cb..9eacbe0b25e8bb09089f393d41166bf1d3e1cd90 100644 --- a/features/explore/groups.feature +++ b/features/explore/groups.feature @@ -24,14 +24,6 @@ Feature: Explore Groups Then I should see project "Internal" items And I should not see project "Enterprise" items - Scenario: I should see group's members as user - Given group "TestGroup" has internal project "Internal" - And "John Doe" is owner of group "TestGroup" - When I sign in as a user - And I visit group "TestGroup" members page - Then I should see group member "John Doe" - And I should not see member roles - Scenario: I should see group with private, internal and public projects as visitor Given group "TestGroup" has internal project "Internal" Given group "TestGroup" has public project "Community" @@ -56,14 +48,6 @@ Feature: Explore Groups And I should not see project "Internal" items And I should not see project "Enterprise" items - Scenario: I should see group's members as visitor - Given group "TestGroup" has internal project "Internal" - Given group "TestGroup" has public project "Community" - And "John Doe" is owner of group "TestGroup" - When I visit group "TestGroup" members page - Then I should see group member "John Doe" - And I should not see member roles - Scenario: I should see group with private, internal and public projects as user Given group "TestGroup" has internal project "Internal" Given group "TestGroup" has public project "Community" @@ -91,15 +75,6 @@ Feature: Explore Groups And I should see project "Internal" items And I should not see project "Enterprise" items - Scenario: I should see group's members as user - Given group "TestGroup" has internal project "Internal" - Given group "TestGroup" has public project "Community" - And "John Doe" is owner of group "TestGroup" - When I sign in as a user - And I visit group "TestGroup" members page - Then I should see group member "John Doe" - And I should not see member roles - Scenario: I should see group with public project in public groups area Given group "TestGroup" has public project "Community" When I visit the public groups area diff --git a/features/steps/explore/groups.rb b/features/steps/explore/groups.rb index 87f32e70d59257c2b9365c692451d12ad55848d1..409bf0cb4167cbefc7af0c14d66bb318b4f2a5cf 100644 --- a/features/steps/explore/groups.rb +++ b/features/steps/explore/groups.rb @@ -62,10 +62,6 @@ class Spinach::Features::ExploreGroups < Spinach::FeatureSteps expect(page).to have_content "John Doe" end - step 'I should not see member roles' do - expect(body).not_to match(%r{owner|developer|reporter|guest}i) - end - protected def group_has_project(groupname, projectname, visibility_level) diff --git a/features/steps/project/badges/build.rb b/features/steps/project/badges/build.rb index 66a48a176e58703e062660a7cd51e2f2f6957e47..96c59322f9b2f59d7a0231a836c2d5bb751f996b 100644 --- a/features/steps/project/badges/build.rb +++ b/features/steps/project/badges/build.rb @@ -26,7 +26,7 @@ class Spinach::Features::ProjectBadgesBuild < Spinach::FeatureSteps def expect_badge(status) svg = Nokogiri::XML.parse(page.body) - expect(page.response_headers).to include('Content-Type' => 'image/svg+xml') + expect(page.response_headers['Content-Type']).to include('image/svg+xml') expect(svg.at(%Q{text:contains("#{status}")})).to be_truthy end end diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb index 4d6b258f5778d11876204e144ceb50656cc8d993..70e6d4836b2463703c66c28d8ca36aa8be455271 100644 --- a/features/steps/shared/builds.rb +++ b/features/steps/shared/builds.rb @@ -10,20 +10,20 @@ module SharedBuilds end step 'project has a recent build' do - @pipeline = create(:ci_pipeline, project: @project, sha: @project.commit.sha, ref: 'master') + @pipeline = create(:ci_empty_pipeline, project: @project, sha: @project.commit.sha, ref: 'master') @build = create(:ci_build_with_coverage, pipeline: @pipeline) end step 'recent build is successful' do - @build.update(status: 'success') + @build.success end step 'recent build failed' do - @build.update(status: 'failed') + @build.drop end step 'project has another build that is running' do - create(:ci_build, pipeline: @pipeline, name: 'second build', status: 'running') + create(:ci_build, pipeline: @pipeline, name: 'second build', status_event: 'run') end step 'I visit recent build details page' do diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb new file mode 100644 index 0000000000000000000000000000000000000000..d02b469dac8bb25014fa3ea7c261e439e94553ac --- /dev/null +++ b/lib/api/access_requests.rb @@ -0,0 +1,90 @@ +module API + class AccessRequests < Grape::API + before { authenticate! } + + helpers ::API::Helpers::MembersHelpers + + %w[group project].each do |source_type| + resource source_type.pluralize do + # Get a list of group/project access requests viewable by the authenticated user. + # + # Parameters: + # id (required) - The group/project ID + # + # Example Request: + # GET /groups/:id/access_requests + # GET /projects/:id/access_requests + get ":id/access_requests" do + source = find_source(source_type, params[:id]) + authorize_admin_source!(source_type, source) + + access_requesters = paginate(source.requesters.includes(:user)) + + present access_requesters.map(&:user), with: Entities::AccessRequester, access_requesters: access_requesters + end + + # Request access to the group/project + # + # Parameters: + # id (required) - The group/project ID + # + # Example Request: + # POST /groups/:id/access_requests + # POST /projects/:id/access_requests + post ":id/access_requests" do + source = find_source(source_type, params[:id]) + access_requester = source.request_access(current_user) + + if access_requester.persisted? + present access_requester.user, with: Entities::AccessRequester, access_requester: access_requester + else + render_validation_error!(access_requester) + end + end + + # Approve a group/project access request + # + # Parameters: + # id (required) - The group/project ID + # user_id (required) - The user ID of the access requester + # access_level (optional) - Access level + # + # Example Request: + # PUT /groups/:id/access_requests/:user_id/approve + # PUT /projects/:id/access_requests/:user_id/approve + put ':id/access_requests/:user_id/approve' do + required_attributes! [:user_id] + source = find_source(source_type, params[:id]) + authorize_admin_source!(source_type, source) + + member = source.requesters.find_by!(user_id: params[:user_id]) + if params[:access_level] + member.update(access_level: params[:access_level]) + end + member.accept_request + + status :created + present member.user, with: Entities::Member, member: member + end + + # Deny a group/project access request + # + # Parameters: + # id (required) - The group/project ID + # user_id (required) - The user ID of the access requester + # + # Example Request: + # DELETE /groups/:id/access_requests/:user_id + # DELETE /projects/:id/access_requests/:user_id + delete ":id/access_requests/:user_id" do + required_attributes! [:user_id] + source = find_source(source_type, params[:id]) + + access_requester = source.requesters.find_by!(user_id: params[:user_id]) + + ::Members::DestroyService.new(access_requester, current_user).execute + end + end + end + end +end diff --git a/lib/api/api.rb b/lib/api/api.rb index b0003728bdb75e54557346de4208af596bf298ea..9921d6f85705f2ced28bd4e7e608a6e952185674 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -3,12 +3,18 @@ class API < Grape::API include APIGuard version 'v3', using: :path + rescue_from Gitlab::Access::AccessDeniedError do + rack_response({ 'message' => '403 Forbidden' }.to_json, 403) + end + rescue_from ActiveRecord::RecordNotFound do rack_response({ 'message' => '404 Not found' }.to_json, 404) end - rescue_from Grape::Exceptions::ValidationErrors do |e| - error!({ messages: e.full_messages }, 400) + # Retain 405 error rather than a 500 error for Grape 0.15.0+. + # See: https://github.com/ruby-grape/grape/commit/252bfd27c320466ec3c0751812cf44245e97e5de + rescue_from Grape::Exceptions::Base do |e| + error! e.message, e.status, e.headers end rescue_from :all do |exception| @@ -30,6 +36,7 @@ class API < Grape::API # Ensure the namespace is right, otherwise we might load Grape::API::Helpers helpers ::API::Helpers + mount ::API::AccessRequests mount ::API::AwardEmoji mount ::API::Branches mount ::API::Builds @@ -38,7 +45,6 @@ class API < Grape::API mount ::API::DeployKeys mount ::API::Environments mount ::API::Files - mount ::API::GroupMembers mount ::API::Groups mount ::API::Geo mount ::API::Internal @@ -49,6 +55,7 @@ class API < Grape::API mount ::API::LicenseTemplates mount ::API::Ldap mount ::API::LdapGroupLinks + mount ::API::Members mount ::API::MergeRequests mount ::API::Milestones mount ::API::Namespaces diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index 5c570b5e5ca375e1718a721576444116d44fd017..825e05fbae3d72c930b3bff691d2339b796c4c4f 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -10,6 +10,9 @@ class DeployKeys < Grape::API present keys, with: Entities::SSHKey end + params do + requires :id, type: String, desc: 'The ID of the project' + end resource :projects do before { authorize_admin_project } @@ -17,52 +20,43 @@ class DeployKeys < Grape::API # Use "projects/:id/deploy_keys/..." instead. # %w(keys deploy_keys).each do |path| - # Get a specific project's deploy keys - # - # Example Request: - # GET /projects/:id/deploy_keys + desc "Get a specific project's deploy keys" do + success Entities::SSHKey + end get ":id/#{path}" do present user_project.deploy_keys, with: Entities::SSHKey end - # Get single deploy key owned by currently authenticated user - # - # Example Request: - # GET /projects/:id/deploy_keys/:key_id + desc 'Get single deploy key' do + success Entities::SSHKey + end + params do + requires :key_id, type: Integer, desc: 'The ID of the deploy key' + end get ":id/#{path}/:key_id" do key = user_project.deploy_keys.find params[:key_id] present key, with: Entities::SSHKey end - # Add new deploy key to currently authenticated user - # If deploy key already exists - it will be joined to project - # but only if original one was accessible by same user - # - # Parameters: - # key (required) - New deploy Key - # title (required) - New deploy Key's title - # Example Request: - # POST /projects/:id/deploy_keys + # TODO: for 9.0 we should check if params are there with the params block + # grape provides, at this point we'd change behaviour so we can't + # Behaviour now if you don't provide all required params: it renders a + # validation error or two. + desc 'Add new deploy key to currently authenticated user' do + success Entities::SSHKey + end post ":id/#{path}" do attrs = attributes_for_keys [:title, :key] + attrs[:key].strip! if attrs[:key] - if attrs[:key].present? - attrs[:key].strip! - - # check if key already exist in project - key = user_project.deploy_keys.find_by(key: attrs[:key]) - if key - present key, with: Entities::SSHKey - next - end + key = user_project.deploy_keys.find_by(key: attrs[:key]) + present key, with: Entities::SSHKey if key - # Check for available deploy keys in other projects - key = current_user.accessible_deploy_keys.find_by(key: attrs[:key]) - if key - user_project.deploy_keys << key - present key, with: Entities::SSHKey - next - end + # Check for available deploy keys in other projects + key = current_user.accessible_deploy_keys.find_by(key: attrs[:key]) + if key + user_project.deploy_keys << key + present key, with: Entities::SSHKey end key = DeployKey.new attrs @@ -74,12 +68,46 @@ class DeployKeys < Grape::API end end - # Delete existing deploy key of currently authenticated user - # - # Example Request: - # DELETE /projects/:id/deploy_keys/:key_id + desc 'Enable a deploy key for a project' do + detail 'This feature was added in GitLab 8.11' + success Entities::SSHKey + end + params do + requires :key_id, type: Integer, desc: 'The ID of the deploy key' + end + post ":id/#{path}/:key_id/enable" do + key = ::Projects::EnableDeployKeyService.new(user_project, + current_user, declared(params)).execute + + if key + present key, with: Entities::SSHKey + else + not_found!('Deploy Key') + end + end + + desc 'Disable a deploy key for a project' do + detail 'This feature was added in GitLab 8.11' + success Entities::SSHKey + end + params do + requires :key_id, type: Integer, desc: 'The ID of the deploy key' + end + delete ":id/#{path}/:key_id/disable" do + key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id]) + key.destroy + + present key.deploy_key, with: Entities::SSHKey + end + + desc 'Delete existing deploy key of currently authenticated user' do + success Key + end + params do + requires :key_id, type: Integer, desc: 'The ID of the deploy key' + end delete ":id/#{path}/:key_id" do - key = user_project.deploy_keys.find params[:key_id] + key = user_project.deploy_keys.find(params[:key_id]) key.destroy end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 6edcc7ddc366c89a849df9159d08f9af1b8f173d..3b3486816b32b0d09e0dd5876ce57792afbeb7f3 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -97,9 +97,17 @@ class Project < Grape::Entity expose :repository_storage, if: lambda { |_project, options| options[:user].try(:admin?) } end - class ProjectMember < UserBasic + class Member < UserBasic expose :access_level do |user, options| - options[:project].project_members.find_by(user_id: user.id).access_level + member = options[:member] || options[:members].find { |m| m.user_id == user.id } + member.access_level + end + end + + class AccessRequester < UserBasic + expose :requested_at do |user, options| + access_requester = options[:access_requester] || options[:access_requesters].find { |m| m.user_id == user.id } + access_requester.requested_at end end @@ -124,12 +132,6 @@ class GroupDetail < Group expose :shared_projects, using: Entities::Project end - class GroupMember < UserBasic - expose :access_level do |user, options| - options[:group].group_members.find_by(user_id: user.id).access_level - end - end - class RepoBranch < Grape::Entity expose :name @@ -357,7 +359,7 @@ class Namespace < Grape::Entity expose :id, :path, :kind end - class Member < Grape::Entity + class MemberAccess < Grape::Entity expose :access_level expose :notification_level do |member, options| if member.notification_setting @@ -366,10 +368,10 @@ class Member < Grape::Entity end end - class ProjectAccess < Member + class ProjectAccess < MemberAccess end - class GroupAccess < Member + class GroupAccess < MemberAccess end class ProjectService < Grape::Entity diff --git a/lib/api/group_members.rb b/lib/api/group_members.rb deleted file mode 100644 index dbe5bb08d3ff05bf22e8de31acbd80ee8967c175..0000000000000000000000000000000000000000 --- a/lib/api/group_members.rb +++ /dev/null @@ -1,87 +0,0 @@ -module API - class GroupMembers < Grape::API - before { authenticate! } - - resource :groups do - # Get a list of group members viewable by the authenticated user. - # - # Example Request: - # GET /groups/:id/members - get ":id/members" do - group = find_group(params[:id]) - users = group.users - present users, with: Entities::GroupMember, group: group - end - - # Add a user to the list of group members - # - # Parameters: - # id (required) - group id - # user_id (required) - the users id - # access_level (required) - Project access level - # Example Request: - # POST /groups/:id/members - post ":id/members" do - group = find_group(params[:id]) - authorize! :admin_group, group - required_attributes! [:user_id, :access_level] - - unless validate_access_level?(params[:access_level]) - render_api_error!("Wrong access level", 422) - end - - if group.group_members.find_by(user_id: params[:user_id]) - render_api_error!("Already exists", 409) - end - - group.add_users([params[:user_id]], params[:access_level], current_user) - member = group.group_members.find_by(user_id: params[:user_id]) - present member.user, with: Entities::GroupMember, group: group - end - - # Update group member - # - # Parameters: - # id (required) - The ID of a group - # user_id (required) - The ID of a group member - # access_level (required) - Project access level - # Example Request: - # PUT /groups/:id/members/:user_id - put ':id/members/:user_id' do - group = find_group(params[:id]) - authorize! :admin_group, group - required_attributes! [:access_level] - - group_member = group.group_members.find_by(user_id: params[:user_id]) - not_found!('User can not be found') if group_member.nil? - - if group_member.update_attributes(access_level: params[:access_level]) - @member = group_member.user - present @member, with: Entities::GroupMember, group: group - else - handle_member_errors group_member.errors - end - end - - # Remove member. - # - # Parameters: - # id (required) - group id - # user_id (required) - the users id - # - # Example Request: - # DELETE /groups/:id/members/:user_id - delete ":id/members/:user_id" do - group = find_group(params[:id]) - authorize! :admin_group, group - member = group.group_members.find_by(user_id: params[:user_id]) - - if member.nil? - render_api_error!("404 Not Found - user_id:#{params[:user_id]} not a member of group #{group.name}", 404) - else - member.destroy - end - end - end - end -end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 54df2c0a2bfa67b62d3675675580c60773f815d3..6e38c1983f6127bd949c34d307e9ecd2b511abfc 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -28,7 +28,7 @@ def current_user # If the sudo is the current user do nothing if identifier && !(@current_user.id == identifier || @current_user.username == identifier) - render_api_error!('403 Forbidden: Must be admin to use sudo', 403) unless @current_user.is_admin? + forbidden!('Must be admin to use sudo') unless @current_user.is_admin? @current_user = User.by_username_or_id(identifier) not_found!("No user id or username for: #{identifier}") if @current_user.nil? end @@ -49,16 +49,15 @@ def sudo_identifier def user_project @project ||= find_project(params[:id]) - @project || not_found!("Project") end def find_project(id) project = Project.find_with_namespace(id) || Project.find_by(id: id) - if project && can?(current_user, :read_project, project) + if can?(current_user, :read_project, project) project else - nil + not_found!('Project') end end @@ -89,11 +88,7 @@ def service_attributes end def find_group(id) - begin - group = Group.find(id) - rescue ActiveRecord::RecordNotFound - group = Group.find_by!(path: id) - end + group = Group.find_by(path: id) || Group.find_by(id: id) if can?(current_user, :read_group, group) group @@ -142,7 +137,7 @@ def authenticated_as_admin! end def authorize!(action, subject) - forbidden! unless abilities.allowed?(current_user, action, subject) + forbidden! unless can?(current_user, action, subject) end def authorize_push_project @@ -204,10 +199,6 @@ def validate_label_params(params) errors end - def validate_access_level?(level) - Gitlab::Access.options_with_owner.values.include? level.to_i - end - # Checks the occurrences of datetime attributes, each attribute if present in the params hash must be in ISO 8601 # format (YYYY-MM-DDTHH:MM:SSZ) or a Bad Request error is invoked. # diff --git a/lib/api/helpers/members_helpers.rb b/lib/api/helpers/members_helpers.rb new file mode 100644 index 0000000000000000000000000000000000000000..90114f6f6677de987ba665d356ed318318508199 --- /dev/null +++ b/lib/api/helpers/members_helpers.rb @@ -0,0 +1,13 @@ +module API + module Helpers + module MembersHelpers + def find_source(source_type, id) + public_send("find_#{source_type}", id) + end + + def authorize_admin_source!(source_type, source) + authorize! :"admin_#{source_type}", source + end + end + end +end diff --git a/lib/api/internal.rb b/lib/api/internal.rb index eddddbdc12ea06640d3a0d570b8994b5c306081d..e3ad5e0ee66a5cfac7305dd0337dc4a9bd12a20d 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -74,6 +74,10 @@ def project response end + get "/merge_request_urls" do + ::MergeRequests::GetUrlsService.new(project).execute(params[:changes]) + end + # # Get a ssh key using the fingerprint # diff --git a/lib/api/members.rb b/lib/api/members.rb new file mode 100644 index 0000000000000000000000000000000000000000..2fae83f60b278aace40315c85f401c41789ce776 --- /dev/null +++ b/lib/api/members.rb @@ -0,0 +1,155 @@ +module API + class Members < Grape::API + before { authenticate! } + + helpers ::API::Helpers::MembersHelpers + + %w[group project].each do |source_type| + resource source_type.pluralize do + # Get a list of group/project members viewable by the authenticated user. + # + # Parameters: + # id (required) - The group/project ID + # query - Query string + # + # Example Request: + # GET /groups/:id/members + # GET /projects/:id/members + get ":id/members" do + source = find_source(source_type, params[:id]) + + members = source.members.includes(:user) + members = members.joins(:user).merge(User.search(params[:query])) if params[:query] + members = paginate(members) + + present members.map(&:user), with: Entities::Member, members: members + end + + # Get a group/project member + # + # Parameters: + # id (required) - The group/project ID + # user_id (required) - The user ID of the member + # + # Example Request: + # GET /groups/:id/members/:user_id + # GET /projects/:id/members/:user_id + get ":id/members/:user_id" do + source = find_source(source_type, params[:id]) + + members = source.members + member = members.find_by!(user_id: params[:user_id]) + + present member.user, with: Entities::Member, member: member + end + + # Add a new group/project member + # + # Parameters: + # id (required) - The group/project ID + # user_id (required) - The user ID of the new member + # access_level (required) - A valid access level + # + # Example Request: + # POST /groups/:id/members + # POST /projects/:id/members + post ":id/members" do + source = find_source(source_type, params[:id]) + authorize_admin_source!(source_type, source) + required_attributes! [:user_id, :access_level] + + access_requester = source.requesters.find_by(user_id: params[:user_id]) + if access_requester + # We pass current_user = access_requester so that the requester doesn't + # receive a "access denied" email + ::Members::DestroyService.new(access_requester, access_requester.user).execute + end + + member = source.members.find_by(user_id: params[:user_id]) + + # This is to ensure back-compatibility but 409 behavior should be used + # for both project and group members in 9.0! + conflict!('Member already exists') if source_type == 'group' && member + + unless member + source.add_user(params[:user_id], params[:access_level], current_user) + member = source.members.find_by(user_id: params[:user_id]) + end + + if member + present member.user, with: Entities::Member, member: member + else + # Since `source.add_user` doesn't return a member object, we have to + # build a new one and populate its errors in order to render them. + member = source.members.build(attributes_for_keys([:user_id, :access_level])) + member.valid? # populate the errors + + # This is to ensure back-compatibility but 400 behavior should be used + # for all validation errors in 9.0! + render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level) + render_validation_error!(member) + end + end + + # Update a group/project member + # + # Parameters: + # id (required) - The group/project ID + # user_id (required) - The user ID of the member + # access_level (required) - A valid access level + # + # Example Request: + # PUT /groups/:id/members/:user_id + # PUT /projects/:id/members/:user_id + put ":id/members/:user_id" do + source = find_source(source_type, params[:id]) + authorize_admin_source!(source_type, source) + required_attributes! [:user_id, :access_level] + + member = source.members.find_by!(user_id: params[:user_id]) + + if member.update_attributes(access_level: params[:access_level]) + present member.user, with: Entities::Member, member: member + else + # This is to ensure back-compatibility but 400 behavior should be used + # for all validation errors in 9.0! + render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level) + render_validation_error!(member) + end + end + + # Remove a group/project member + # + # Parameters: + # id (required) - The group/project ID + # user_id (required) - The user ID of the member + # + # Example Request: + # DELETE /groups/:id/members/:user_id + # DELETE /projects/:id/members/:user_id + delete ":id/members/:user_id" do + source = find_source(source_type, params[:id]) + required_attributes! [: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(member, current_user).execute + + present member.user, with: Entities::Member, member: member + end + end + end + end + end +end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 91afd84bb2009fe63fbe6fb816349788254a5af9..1a91c4547cc0f7263edf0276038898bfa7af5510 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -329,7 +329,7 @@ def map_public_to_visibility_level(attrs) # DELETE /projects/:id delete ":id" do authorize! :remove_project, user_project - ::Projects::DestroyService.new(user_project, current_user, {}).pending_delete! + ::Projects::DestroyService.new(user_project, current_user, {}).async_execute end # Mark this project as forked from another diff --git a/lib/backup/files.rb b/lib/backup/files.rb index 654b4d1c8962dc35300e7590c90e3cdf88dc9baa..cedbb289f6a80be0ac52a9923b673fd2f8ab4b1a 100644 --- a/lib/backup/files.rb +++ b/lib/backup/files.rb @@ -27,7 +27,7 @@ def restore def backup_existing_files_dir timestamped_files_path = File.join(files_parent_dir, "#{name}.#{Time.now.to_i}") - if File.exists?(app_files_dir) + if File.exist?(app_files_dir) FileUtils.mv(app_files_dir, File.expand_path(timestamped_files_path)) end end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index e47ca01862b642b107095399a2d036e33593bb1f..261ab7e27df11bbe67316f37deb5183009dab463 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -114,7 +114,7 @@ def unpack tar_file = ENV["BACKUP"].nil? ? File.join("#{file_list.first}_gitlab_backup.tar") : File.join(ENV["BACKUP"] + "_gitlab_backup.tar") - unless File.exists?(tar_file) + unless File.exist?(tar_file) puts "The specified backup doesn't exist!" exit 1 end diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index 1f5917b81275367420a109aa7cd29fad214d9a67..f117fc3d37def0815360c09e4221dbadea2a9f11 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -28,7 +28,7 @@ def dump wiki = ProjectWiki.new(project) - if File.exists?(path_to_repo(wiki)) + if File.exist?(path_to_repo(wiki)) $progress.print " * #{wiki.path_with_namespace} ... " if wiki.repository.empty? $progress.puts " [SKIPPED]".color(:cyan) @@ -49,7 +49,7 @@ def dump def restore Gitlab.config.repositories.storages.each do |name, path| - next unless File.exists?(path) + next unless File.exist?(path) # Move repos dir to 'repositories.old' dir bk_repos_path = File.join(path, '..', 'repositories.old.' + Time.now.to_i.to_s) @@ -63,7 +63,7 @@ def restore project.ensure_dir_exist - if File.exists?(path_to_bundle(project)) + if File.exist?(path_to_bundle(project)) FileUtils.mkdir_p(path_to_repo(project)) cmd = %W(tar -xf #{path_to_bundle(project)} -C #{path_to_repo(project)}) else @@ -80,7 +80,7 @@ def restore wiki = ProjectWiki.new(project) - if File.exists?(path_to_bundle(wiki)) + if File.exist?(path_to_bundle(wiki)) $progress.print " * #{wiki.path_with_namespace} ... " # If a wiki bundle exists, first remove the empty repo diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb index 46762d401fb9ce4deed8d8a9e0b829d9452b5db3..4fa8d05481f991cb7ba62c58f24d05abf2be635f 100644 --- a/lib/banzai/filter/relative_link_filter.rb +++ b/lib/banzai/filter/relative_link_filter.rb @@ -52,7 +52,7 @@ def rebuild_relative_uri(uri) relative_url_root, context[:project].path_with_namespace, uri_type(file_path), - ref || context[:project].default_branch, # if no ref exists, point to the default branch + ref, file_path ].compact.join('/').squeeze('/').chomp('/') @@ -116,7 +116,7 @@ def uri_type(path) end def current_commit - @current_commit ||= context[:commit] || ref ? repository.commit(ref) : repository.head_commit + @current_commit ||= context[:commit] || repository.commit(ref) end def relative_url_root @@ -124,7 +124,7 @@ def relative_url_root end def ref - context[:ref] + context[:ref] || context[:project].default_branch end def repository diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index a2e8bd22a525786bc0bac9aa8a62303131e4598b..47efd5bd9f264eb3685d0f07e252bff49dedb8f5 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -62,7 +62,7 @@ def build_attributes(name) # - before script should be a concatenated command commands: [job[:before_script] || @before_script, job[:script]].flatten.compact.join("\n"), tag_list: job[:tags] || [], - name: job[:name], + name: job[:name].to_s, allow_failure: job[:allow_failure] || false, when: job[:when] || 'on_success', environment: job[:environment], diff --git a/lib/ci/static_model.rb b/lib/ci/static_model.rb deleted file mode 100644 index bb2bdbed49519850058a2ba4d9127ba760ad885b..0000000000000000000000000000000000000000 --- a/lib/ci/static_model.rb +++ /dev/null @@ -1,49 +0,0 @@ -# Provides an ActiveRecord-like interface to a model whose data is not persisted to a database. -module Ci - module StaticModel - extend ActiveSupport::Concern - - module ClassMethods - # Used by ActiveRecord's polymorphic association to set object_id - def primary_key - 'id' - end - - # Used by ActiveRecord's polymorphic association to set object_type - def base_class - self - end - end - - # Used by AR for fetching attributes - # - # Pass it along if we respond to it. - def [](key) - send(key) if respond_to?(key) - end - - def to_param - id - end - - def new_record? - false - end - - def persisted? - false - end - - def destroyed? - false - end - - def ==(other) - if other.is_a? ::Ci::StaticModel - id == other.id - else - super - end - end - end -end diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb index 51e46da82ccd24dd61ff938167bc728518d0820f..84688f6646eb849c0e84ee097491404249d63c42 100644 --- a/lib/extracts_path.rb +++ b/lib/extracts_path.rb @@ -94,7 +94,7 @@ def assign_ref_vars @options = params.select {|key, value| allowed_options.include?(key) && !value.blank? } @options = HashWithIndifferentAccess.new(@options) - @id = Addressable::URI.unescape(get_id) + @id = Addressable::URI.normalize_component(get_id) @ref, @path = extract_ref(@id) @repo = @project.repository if @options[:extended_sha1].blank? diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb deleted file mode 100644 index 056d83acac0075699be85d4a957aca2afd98acfa..0000000000000000000000000000000000000000 --- a/lib/gitlab/backend/grack_auth.rb +++ /dev/null @@ -1,286 +0,0 @@ -module Grack - class AuthSpawner - def self.call(env) - # Avoid issues with instance variables in Grack::Auth persisting across - # requests by creating a new instance for each request. - Auth.new({}).call_with_kerberos_support(env) - end - end - - class Auth < Rack::Auth::Basic - attr_accessor :user, :project, :env - - def call_with_kerberos_support(env) - # Make sure the final leg of Kerberos authentication is applied as per RFC4559 - apply_negotiate_final_leg(call(env)) - end - - def call(env) - @env = env - @request = Rack::Request.new(env) - @auth = Request.new(env) - - @ci = false - - # Need this patch due to the rails mount - # Need this if under RELATIVE_URL_ROOT - unless Gitlab.config.gitlab.relative_url_root.empty? - # If website is mounted using relative_url_root need to remove it first - @env['PATH_INFO'] = @request.path.sub(Gitlab.config.gitlab.relative_url_root, '') - else - @env['PATH_INFO'] = @request.path - end - - @env['SCRIPT_NAME'] = "" - - auth! - - lfs_response = Gitlab::Lfs::Router.new(project, @user, @ci, @request).try_call - return lfs_response unless lfs_response.nil? - - if @user.nil? && !@ci - unauthorized - else - render_not_found - end - end - - private - - def allow_basic_auth? - return true unless Gitlab.config.kerberos.enabled && - Gitlab.config.kerberos.use_dedicated_port && - @env['SERVER_PORT'] == Gitlab.config.kerberos.port.to_s - end - - def allow_kerberos_auth? - return false unless Gitlab.config.kerberos.enabled - return true unless Gitlab.config.kerberos.use_dedicated_port - # When using a dedicated port, allow Kerberos auth only if port matches the configured one - @env['SERVER_PORT'] == Gitlab.config.kerberos.port.to_s - end - - def spnego_challenge - return "Negotiate" unless @auth.spnego_response_token - "Negotiate #{::Base64.strict_encode64(@auth.spnego_response_token)}" - end - - def challenge - challenges = [] - challenges << super if allow_basic_auth? - challenges << spnego_challenge if allow_kerberos_auth? - # Use \n separator to generate multiple WWW-Authenticate headers in case of multiple challenges - challenges.join("\n") - end - - def apply_negotiate_final_leg(response) - return response unless allow_kerberos_auth? && @auth.spnego_response_token - # As per RFC4559, we may have a final WWW-Authenticate header to send in - # the response even if it's not a 401 status - status, headers, body = response - headers['WWW-Authenticate'] = spnego_challenge - - [status, headers, body] - end - - def valid_auth_method? - (allow_basic_auth? && @auth.basic?) || (allow_kerberos_auth? && @auth.negotiate?) - end - - def auth! - return unless @auth.provided? - - return bad_request unless valid_auth_method? - - if @auth.negotiate? - # Authentication with Kerberos token - krb_principal = @auth.spnego_credentials! - return unless krb_principal - - # Set @user if authentication succeeded - identity = ::Identity.find_by(provider: :kerberos, extern_uid: krb_principal) - @user = identity.user if identity - else - # Authentication with username and password - login, password = @auth.credentials - - # Allow authentication for GitLab CI service - # if valid token passed - if ci_request?(login, password) - @ci = true - return - end - - @user = authenticate_user(login, password) - end - end - - def ci_request?(login, password) - matched_login = /(?^[a-zA-Z]*-ci)-token$/.match(login) - - if project && matched_login.present? - underscored_service = matched_login['s'].underscore - - if underscored_service == 'gitlab_ci' - return project && project.valid_build_token?(password) - elsif Service.available_services_names.include?(underscored_service) - service_method = "#{underscored_service}_service" - service = project.send(service_method) - - return service && service.activated? && service.valid_token?(password) - end - end - - false - end - - def oauth_access_token_check(login, password) - if login == "oauth2" && git_cmd == 'git-upload-pack' && password.present? - token = Doorkeeper::AccessToken.by_token(password) - token && token.accessible? && User.find_by(id: token.resource_owner_id) - end - end - - def authenticate_user(login, password) - user = Gitlab::Auth.find_with_user_password(login, password) - - unless user - user = oauth_access_token_check(login, password) - end - - # If the user authenticated successfully, we reset the auth failure count - # from Rack::Attack for that IP. A client may attempt to authenticate - # with a username and blank password first, and only after it receives - # a 401 error does it present a password. Resetting the count prevents - # false positives from occurring. - # - # Otherwise, we let Rack::Attack know there was a failed authentication - # attempt from this IP. This information is stored in the Rails cache - # (Redis) and will be used by the Rack::Attack middleware to decide - # whether to block requests from this IP. - config = Gitlab.config.rack_attack.git_basic_auth - - if config.enabled - if user - # A successful login will reset the auth failure count from this IP - Rack::Attack::Allow2Ban.reset(@request.ip, config) - else - banned = Rack::Attack::Allow2Ban.filter(@request.ip, config) do - # Unless the IP is whitelisted, return true so that Allow2Ban - # increments the counter (stored in Rails.cache) for the IP - if config.ip_whitelist.include?(@request.ip) - false - else - true - end - end - - if banned - Rails.logger.info "IP #{@request.ip} failed to login " \ - "as #{login} but has been temporarily banned from Git auth" - end - end - end - - user - end - - def authorized_request? - return true if @ci - - case git_cmd - when *Gitlab::GitAccess::DOWNLOAD_COMMANDS - if !Gitlab.config.gitlab_shell.upload_pack - false - elsif user - Gitlab::GitAccess.new(user, project).download_access_check.allowed? - elsif project.public? - # Allow clone/fetch for public projects - true - else - false - end - when *Gitlab::GitAccess::PUSH_COMMANDS - if !Gitlab.config.gitlab_shell.receive_pack - false - elsif user - # Skip user authorization on upload request. - # It will be done by the pre-receive hook in the repository. - true - else - false - end - else - false - end - end - - def git_cmd - if @request.get? - @request.params['service'] - elsif @request.post? - File.basename(@request.path) - else - nil - end - end - - def project - return @project if defined?(@project) - - @project = project_by_path(@request.path_info) - end - - def project_by_path(path) - if m = /^([\w\.\/-]+)\.git/.match(path).to_a - path_with_namespace = m.last - path_with_namespace.gsub!(/\.wiki$/, '') - - path_with_namespace[0] = '' if path_with_namespace.start_with?('/') - Project.find_with_namespace(path_with_namespace) - end - end - - def render_not_found - [404, { "Content-Type" => "text/plain" }, ["Not Found"]] - end - - class Request < Rack::Auth::Basic::Request - attr_reader :spnego_response_token - - def negotiate? - parts.first && scheme == "negotiate" - end - - def spnego_token - ::Base64.strict_decode64(params) - end - - def spnego_credentials! - require 'gssapi' - gss = GSSAPI::Simple.new(nil, nil, Gitlab.config.kerberos.keytab) - # the GSSAPI::Simple constructor transforms a nil service name into a default value, so - # pass service name to acquire_credentials explicitly to support the special meaning of nil - gss_service_name = - if Gitlab.config.kerberos.service_principal_name.present? - gss.import_name(Gitlab.config.kerberos.service_principal_name) - else - nil # accept any valid service principal name from keytab - end - gss.acquire_credentials(gss_service_name) # grab credentials from keytab - - # Decode token - gss_result = gss.accept_context(spnego_token) - - # gss_result will be 'true' if nothing has to be returned to the client - @spnego_response_token = gss_result if gss_result && gss_result != true - - # Return user principal name if authentication succeeded - gss.display_name - rescue GSSAPI::GssApiError => ex - Rails.logger.error "#{self.class.name}: failed to process Negotiate/Kerberos authentication: #{ex.message}" - false - end - end - end -end diff --git a/lib/gitlab/badge/build.rb b/lib/gitlab/badge/build.rb index e5e9fab3f5c8c8a7cff53de608a79c95ff199678..1de721a2269633a94f3e0a4071ddebc8da5ed60a 100644 --- a/lib/gitlab/badge/build.rb +++ b/lib/gitlab/badge/build.rb @@ -4,42 +4,26 @@ module Badge # Build badge # class Build - include Gitlab::Application.routes.url_helpers - include ActionView::Helpers::AssetTagHelper - include ActionView::Helpers::UrlHelper + delegate :key_text, :value_text, to: :template def initialize(project, ref) - @project, @ref = project, ref - @image = ::Ci::ImageForBuildService.new.execute(project, ref: ref) + @project = project + @ref = ref + @sha = @project.commit(@ref).try(:sha) end - def type - 'image/svg+xml' + def status + @project.pipelines + .where(sha: @sha, ref: @ref) + .status || 'unknown' end - def data - File.read(@image[:path]) + def metadata + @metadata ||= Build::Metadata.new(@project, @ref) end - def to_s - @image[:name].sub(/\.svg$/, '') - end - - def to_html - link_to(image_tag(image_url, alt: 'build status'), link_url) - end - - def to_markdown - "[![build status](#{image_url})](#{link_url})" - end - - def image_url - build_namespace_project_badges_url(@project.namespace, - @project, @ref, format: :svg) - end - - def link_url - namespace_project_commits_url(@project.namespace, @project, id: @ref) + def template + @template ||= Build::Template.new(status) end end end diff --git a/lib/gitlab/badge/build/metadata.rb b/lib/gitlab/badge/build/metadata.rb new file mode 100644 index 0000000000000000000000000000000000000000..553ef8d7b1616964e379804b248d18310e014314 --- /dev/null +++ b/lib/gitlab/badge/build/metadata.rb @@ -0,0 +1,36 @@ +module Gitlab + module Badge + class Build + ## + # Class that describes build badge metadata + # + class Metadata + include Gitlab::Application.routes.url_helpers + include ActionView::Helpers::AssetTagHelper + include ActionView::Helpers::UrlHelper + + def initialize(project, ref) + @project = project + @ref = ref + end + + def to_html + link_to(image_tag(image_url, alt: 'build status'), link_url) + end + + def to_markdown + "[![build status](#{image_url})](#{link_url})" + end + + def image_url + build_namespace_project_badges_url(@project.namespace, + @project, @ref, format: :svg) + end + + def link_url + namespace_project_commits_url(@project.namespace, @project, id: @ref) + end + end + end + end +end diff --git a/lib/gitlab/badge/build/template.rb b/lib/gitlab/badge/build/template.rb new file mode 100644 index 0000000000000000000000000000000000000000..deba3b669b3af96421779373989e812a26d0fb6e --- /dev/null +++ b/lib/gitlab/badge/build/template.rb @@ -0,0 +1,63 @@ +module Gitlab + module Badge + class Build + ## + # Class that represents a build badge template. + # + # Template object will be passed to badge.svg.erb template. + # + class Template + STATUS_COLOR = { + success: '#4c1', + failed: '#e05d44', + running: '#dfb317', + pending: '#dfb317', + canceled: '#9f9f9f', + skipped: '#9f9f9f', + unknown: '#9f9f9f' + } + + def initialize(status) + @status = status + end + + def key_text + 'build' + end + + def value_text + @status + end + + def key_width + 38 + end + + def value_width + 54 + end + + def key_color + '#555' + end + + def value_color + STATUS_COLOR[@status.to_sym] || + STATUS_COLOR[:unknown] + end + + def key_text_anchor + key_width / 2 + end + + def value_text_anchor + key_width + (value_width / 2) + end + + def width + key_width + value_width + end + end + end + end +end diff --git a/lib/gitlab/changes_list.rb b/lib/gitlab/changes_list.rb new file mode 100644 index 0000000000000000000000000000000000000000..95308aca95f788cadfb7190650b587bf4e44a6ed --- /dev/null +++ b/lib/gitlab/changes_list.rb @@ -0,0 +1,25 @@ +module Gitlab + class ChangesList + include Enumerable + + attr_reader :raw_changes + + def initialize(changes) + @raw_changes = changes.kind_of?(String) ? changes.lines : changes + end + + def each(&block) + changes.each(&block) + end + + def changes + @changes ||= begin + @raw_changes.map do |change| + next if change.blank? + oldrev, newrev, ref = change.strip.split(' ') + { oldrev: oldrev, newrev: newrev, ref: ref } + end.compact + end + end + end +end diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb index f85acd203761e5acfdb8e9c65378f5d8d7fe381b..4542adbcdacdf95abec08a8044034febdcac6d67 100644 --- a/lib/gitlab/checks/change_access.rb +++ b/lib/gitlab/checks/change_access.rb @@ -5,8 +5,8 @@ class ChangeAccess attr_reader :user_access, :project def initialize(change, user_access:, project:) - @oldrev, @newrev, @ref = change.split(' ') - @branch_name = branch_name(@ref) + @oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref) + @branch_name = Gitlab::Git.branch_name(@ref) @user_access = user_access @project = project end @@ -48,7 +48,7 @@ def protected_branch_checks end def tag_checks - tag_ref = tag_name(@ref) + tag_ref = Gitlab::Git.tag_name(@ref) if tag_ref && protected_tag?(tag_ref) && user_access.cannot_do_action?(:admin_project) "You are not allowed to change existing tags on this project." diff --git a/lib/gitlab/ci/config/node/validatable.rb b/lib/gitlab/ci/config/node/validatable.rb index f6e2896dfb23ce2760a62aa46f50a1dbc5ba15e6..085e6e988d1085f59bc9814343b8d4d8d0897868 100644 --- a/lib/gitlab/ci/config/node/validatable.rb +++ b/lib/gitlab/ci/config/node/validatable.rb @@ -7,13 +7,11 @@ module Validatable class_methods do def validator - validator = Class.new(Node::Validator) - - if defined?(@validations) - @validations.each { |rules| validator.class_eval(&rules) } + @validator ||= Class.new(Node::Validator).tap do |validator| + if defined?(@validations) + @validations.each { |rules| validator.class_eval(&rules) } + end end - - validator end private diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb index 9213cfb51e8354ed99768f80f65aec717738981f..a40c44eb1bc5fe1557f68e7848c24167759233b9 100644 --- a/lib/gitlab/email/receiver.rb +++ b/lib/gitlab/email/receiver.rb @@ -1,5 +1,5 @@ -require 'gitlab/email/handler' +require_dependency 'gitlab/email/handler' # Inspired in great part by Discourse's Email::Receiver module Gitlab diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb index 191bea86ac378d1be090c02cb1399e55946a38a7..7584efe4fa864dde1a23c253dde68037841dff58 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -9,6 +9,24 @@ def ref_name(ref) ref.gsub(/\Arefs\/(tags|heads)\//, '') end + def branch_name(ref) + ref = ref.to_s + if self.branch_ref?(ref) + self.ref_name(ref) + else + nil + end + end + + def tag_name(ref) + ref = ref.to_s + if self.tag_ref?(ref) + self.ref_name(ref) + else + nil + end + end + def tag_ref?(ref) ref.start_with?(TAG_REF_PREFIX) end diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index efd5fe78e7f9db5fa01f1b5c9e5729f4fe1e6e68..092875596c7518e48333a396421981b0944f0894 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -17,7 +17,7 @@ def initialize(actor, project, protocol) @user_access = UserAccess.new(user, project: project) end - def check(cmd, changes = nil) + def check(cmd, changes) return build_status_object(false, "Git access over #{protocol.upcase} is not allowed") unless protocol_allowed? unless actor @@ -100,10 +100,10 @@ def user_push_access_check(changes) return build_status_object(false, message) end - changes = changes.lines if changes.kind_of?(String) + changes_list = Gitlab::ChangesList.new(changes) # Iterate over all changes to find if user allowed all of them to be applied - changes.map(&:strip).reject(&:blank?).each do |change| + changes_list.each do |change| status = change_access_check(change) unless status.allowed? # If user does not have access to make at least one change - cancel all push @@ -251,7 +251,7 @@ def user end def build_status_object(status, message = '') - GitAccessStatus.new(status, message) + Gitlab::GitAccessStatus.new(status, message) end def git_annex_access_check(project, changes) diff --git a/lib/gitlab/github_import/branch_formatter.rb b/lib/gitlab/github_import/branch_formatter.rb index 7d2d545b84e50130920a27ec5245375ecda99da0..4750675ae9ddf75afb954de6da314c393c40f18a 100644 --- a/lib/gitlab/github_import/branch_formatter.rb +++ b/lib/gitlab/github_import/branch_formatter.rb @@ -7,10 +7,6 @@ def exists? branch_exists? && commit_exists? end - def name - @name ||= exists? ? ref : "#{ref}-#{short_id}" - end - def valid? repo.present? end diff --git a/lib/gitlab/github_import/hook_formatter.rb b/lib/gitlab/github_import/hook_formatter.rb deleted file mode 100644 index db1fabaa18af50ed0f1d241ac8395361e7d21cc5..0000000000000000000000000000000000000000 --- a/lib/gitlab/github_import/hook_formatter.rb +++ /dev/null @@ -1,23 +0,0 @@ -module Gitlab - module GithubImport - class HookFormatter - EVENTS = %w[* create delete pull_request push].freeze - - attr_reader :raw - - delegate :id, :name, :active, to: :raw - - def initialize(raw) - @raw = raw - end - - def config - raw.config.attrs - end - - def valid? - (EVENTS & raw.events).any? && active - end - end - end -end diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 3932fcb1eda26f05459a1e13cfa3b8f2506c5e9f..9ddc8905bd6149f32bf58fbb896afc45787613c5 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -12,7 +12,6 @@ def initialize(project) if credentials @client = Client.new(credentials[:user]) - @formatter = Gitlab::ImportFormatter.new else raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}" end @@ -66,73 +65,45 @@ def import_issues end def import_pull_requests - disable_webhooks - pull_requests = client.pull_requests(repo, state: :all, sort: :created, direction: :asc, per_page: 100) pull_requests = pull_requests.map { |raw| PullRequestFormatter.new(project, raw) }.select(&:valid?) - source_branches_removed = pull_requests.reject(&:source_branch_exists?).map { |pr| [pr.source_branch_name, pr.source_branch_sha] } - target_branches_removed = pull_requests.reject(&:target_branch_exists?).map { |pr| [pr.target_branch_name, pr.target_branch_sha] } - branches_removed = source_branches_removed | target_branches_removed - - restore_branches(branches_removed) - pull_requests.each do |pull_request| - merge_request = pull_request.create! - apply_labels(merge_request) - import_comments(merge_request) - import_comments_on_diff(merge_request) + begin + restore_source_branch(pull_request) unless pull_request.source_branch_exists? + restore_target_branch(pull_request) unless pull_request.target_branch_exists? + + merge_request = pull_request.create! + apply_labels(merge_request) + import_comments(merge_request) + import_comments_on_diff(merge_request) + rescue ActiveRecord::RecordInvalid => e + raise Projects::ImportService::Error, e.message + ensure + clean_up_restored_branches(pull_request) + end end true - rescue ActiveRecord::RecordInvalid => e - raise Projects::ImportService::Error, e.message - ensure - clean_up_restored_branches(branches_removed) - clean_up_disabled_webhooks end - def disable_webhooks - update_webhooks(hooks, active: false) + def restore_source_branch(pull_request) + project.repository.fetch_ref(repo_url, "pull/#{pull_request.number}/head", pull_request.source_branch_name) end - def clean_up_disabled_webhooks - update_webhooks(hooks, active: true) + def restore_target_branch(pull_request) + project.repository.create_branch(pull_request.target_branch_name, pull_request.target_branch_sha) end - def update_webhooks(hooks, options) - hooks.each do |hook| - client.edit_hook(repo, hook.id, hook.name, hook.config, options) - end + def remove_branch(name) + project.repository.delete_branch(name) + rescue Rugged::ReferenceError + nil end - def hooks - @hooks ||= - begin - client.hooks(repo).map { |raw| HookFormatter.new(raw) }.select(&:valid?) - - # The GitHub Repository Webhooks API returns 404 for users - # without admin access to the repository when listing hooks. - # In this case we just want to return gracefully instead of - # spitting out an error and stop the import process. - rescue Octokit::NotFound - [] - end - end - - def restore_branches(branches) - branches.each do |name, sha| - client.create_ref(repo, "refs/heads/#{name}", sha) - end - - project.repository.fetch_ref(repo_url, '+refs/heads/*', 'refs/heads/*') - end - - def clean_up_restored_branches(branches) - branches.each do |name, _| - client.delete_ref(repo, "heads/#{name}") - project.repository.delete_branch(name) rescue Rugged::ReferenceError - end + def clean_up_restored_branches(pull_request) + remove_branch(pull_request.source_branch_name) unless pull_request.source_branch_exists? + remove_branch(pull_request.target_branch_name) unless pull_request.target_branch_exists? project.repository.after_remove_branch end diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index a4ea2210abdba794f13aa4a869972645229f7712..b84538a090a268a0e97af332dd2528579941ecb1 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -1,8 +1,8 @@ module Gitlab module GithubImport class PullRequestFormatter < BaseFormatter - delegate :exists?, :name, :project, :repo, :sha, to: :source_branch, prefix: true - delegate :exists?, :name, :project, :repo, :sha, to: :target_branch, prefix: true + delegate :exists?, :project, :ref, :repo, :sha, to: :source_branch, prefix: true + delegate :exists?, :project, :ref, :repo, :sha, to: :target_branch, prefix: true def attributes { @@ -33,17 +33,29 @@ def number end def valid? - source_branch.valid? && target_branch.valid? && !cross_project? + source_branch.valid? && target_branch.valid? end def source_branch @source_branch ||= BranchFormatter.new(project, raw_data.head) end + def source_branch_name + @source_branch_name ||= begin + source_branch_exists? ? source_branch_ref : "pull/#{number}/#{source_branch_ref}" + end + end + def target_branch @target_branch ||= BranchFormatter.new(project, raw_data.base) end + def target_branch_name + @target_branch_name ||= begin + target_branch_exists? ? target_branch_ref : "pull/#{number}/#{target_branch_ref}" + end + end + private def assigned? @@ -68,10 +80,6 @@ def body raw_data.body || "" end - def cross_project? - source_branch_repo.id != target_branch_repo.id - end - def description formatter.author_line(author) + body end diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 5e56b3d1aa7fd306277acb35fe0e76b79eb57dc8..b0726268ca617ce6f6807eeb5afef80d38f8da10 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -102,17 +102,19 @@ def generate_imported_object def update_project_references project_id = @relation_hash.delete('project_id') + # If source and target are the same, populate them with the new project ID. + if @relation_hash['source_project_id'] + @relation_hash['source_project_id'] = same_source_and_target? ? project_id : -1 + end + # project_id may not be part of the export, but we always need to populate it if required. @relation_hash['project_id'] = project_id @relation_hash['gl_project_id'] = project_id if @relation_hash['gl_project_id'] @relation_hash['target_project_id'] = project_id if @relation_hash['target_project_id'] - @relation_hash['source_project_id'] = -1 if @relation_hash['source_project_id'] + end - # If source and target are the same, populate them with the new project ID. - if @relation_hash['source_project_id'] && @relation_hash['target_project_id'] && - @relation_hash['target_project_id'] == @relation_hash['source_project_id'] - @relation_hash['source_project_id'] = project_id - end + def same_source_and_target? + @relation_hash['target_project_id'] && @relation_hash['target_project_id'] == @relation_hash['source_project_id'] end def reset_ci_tokens diff --git a/lib/gitlab/lfs/response.rb b/lib/gitlab/lfs/response.rb deleted file mode 100644 index a1ee1aa81ff090de51b6c1db73076ed5261a5dca..0000000000000000000000000000000000000000 --- a/lib/gitlab/lfs/response.rb +++ /dev/null @@ -1,329 +0,0 @@ -module Gitlab - module Lfs - class Response - def initialize(project, user, ci, request) - @origin_project = project - @project = storage_project(project) - @user = user - @ci = ci - @env = request.env - @request = request - end - - def render_download_object_response(oid) - render_response_to_download do - if check_download_sendfile_header? - render_lfs_sendfile(oid) - else - render_not_found - end - end - end - - def render_batch_operation_response - request_body = JSON.parse(@request.body.read) - case request_body["operation"] - when "download" - render_batch_download(request_body) - when "upload" - render_batch_upload(request_body) - else - render_not_found - end - end - - def render_storage_upload_authorize_response(oid, size) - render_response_to_push do - [ - 200, - { "Content-Type" => "application/json; charset=utf-8" }, - [JSON.dump({ - 'StoreLFSPath' => "#{Gitlab.config.lfs.storage_path}/tmp/upload", - 'LfsOid' => oid, - 'LfsSize' => size - })] - ] - end - end - - def render_storage_upload_store_response(oid, size, tmp_file_name) - return render_forbidden unless tmp_file_name - - render_response_to_push do - render_lfs_upload_ok(oid, size, tmp_file_name) - end - end - - def render_unsupported_deprecated_api - [ - 501, - { "Content-Type" => "application/json; charset=utf-8" }, - [JSON.dump({ - 'message' => 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.', - 'documentation_url' => "#{Gitlab.config.gitlab.url}/help", - })] - ] - end - - private - - def render_not_enabled - [ - 501, - { - "Content-Type" => "application/json; charset=utf-8", - }, - [JSON.dump({ - 'message' => 'Git LFS is not enabled on this GitLab server, contact your admin.', - 'documentation_url' => "#{Gitlab.config.gitlab.url}/help", - })] - ] - end - - def render_unauthorized - [ - 401, - { - 'Content-Type' => 'text/plain' - }, - ['Unauthorized'] - ] - end - - def render_not_found - [ - 404, - { - "Content-Type" => "application/vnd.git-lfs+json" - }, - [JSON.dump({ - 'message' => 'Not found.', - 'documentation_url' => "#{Gitlab.config.gitlab.url}/help", - })] - ] - end - - def render_forbidden - [ - 403, - { - "Content-Type" => "application/vnd.git-lfs+json" - }, - [JSON.dump({ - 'message' => 'Access forbidden. Check your access level.', - 'documentation_url' => "#{Gitlab.config.gitlab.url}/help", - })] - ] - end - - def render_lfs_sendfile(oid) - return render_not_found unless oid.present? - - lfs_object = object_for_download(oid) - - if lfs_object && lfs_object.file.exists? - [ - 200, - { - # GitLab-workhorse will forward Content-Type header - "Content-Type" => "application/octet-stream", - "X-Sendfile" => lfs_object.file.path - }, - [] - ] - else - render_not_found - end - end - - def render_batch_upload(body) - return render_not_found if body.empty? || body['objects'].nil? - - render_response_to_push do - response = build_upload_batch_response(body['objects']) - [ - 200, - { - "Content-Type" => "application/json; charset=utf-8", - "Cache-Control" => "private", - }, - [JSON.dump(response)] - ] - end - end - - def render_batch_download(body) - return render_not_found if body.empty? || body['objects'].nil? - - render_response_to_download do - response = build_download_batch_response(body['objects']) - [ - 200, - { - "Content-Type" => "application/json; charset=utf-8", - "Cache-Control" => "private", - }, - [JSON.dump(response)] - ] - end - end - - def render_lfs_upload_ok(oid, size, tmp_file) - if store_file(oid, size, tmp_file) - [ - 200, - { - 'Content-Type' => 'text/plain', - 'Content-Length' => 0 - }, - [] - ] - else - [ - 422, - { 'Content-Type' => 'text/plain' }, - ["Unprocessable entity"] - ] - end - end - - def render_response_to_download - return render_not_enabled unless Gitlab.config.lfs.enabled - - unless @project.public? - return render_unauthorized unless @user || @ci - return render_forbidden unless user_can_fetch? - end - - yield - end - - def render_response_to_push - return render_not_enabled unless Gitlab.config.lfs.enabled - return render_unauthorized unless @user - return render_forbidden unless user_can_push? - - yield - end - - def check_download_sendfile_header? - @env['HTTP_X_SENDFILE_TYPE'].to_s == "X-Sendfile" - end - - def user_can_fetch? - # Check user access against the project they used to initiate the pull - @ci || @user.can?(:download_code, @origin_project) - end - - def user_can_push? - # Check user access against the project they used to initiate the push - @user.can?(:push_code, @origin_project) - end - - def storage_project(project) - if project.forked? - storage_project(project.forked_from_project) - else - project - end - end - - def store_file(oid, size, tmp_file) - tmp_file_path = File.join("#{Gitlab.config.lfs.storage_path}/tmp/upload", tmp_file) - - object = LfsObject.find_or_create_by(oid: oid, size: size) - if object.file.exists? - success = true - else - success = move_tmp_file_to_storage(object, tmp_file_path) - end - - if success - success = link_to_project(object) - end - - success - ensure - # Ensure that the tmp file is removed - FileUtils.rm_f(tmp_file_path) - end - - def object_for_download(oid) - @project.lfs_objects.find_by(oid: oid) - end - - def move_tmp_file_to_storage(object, path) - File.open(path) do |f| - object.file = f - end - - object.file.store! - object.save - end - - def link_to_project(object) - if object && !object.projects.exists?(@project.id) - object.projects << @project - object.save - end - end - - def select_existing_objects(objects) - objects_oids = objects.map { |o| o['oid'] } - @project.lfs_objects.where(oid: objects_oids).pluck(:oid).to_set - end - - def build_upload_batch_response(objects) - selected_objects = select_existing_objects(objects) - - upload_hypermedia_links(objects, selected_objects) - end - - def build_download_batch_response(objects) - selected_objects = select_existing_objects(objects) - - download_hypermedia_links(objects, selected_objects) - end - - def download_hypermedia_links(all_objects, existing_objects) - all_objects.each do |object| - if existing_objects.include?(object['oid']) - object['actions'] = { - 'download' => { - 'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{object['oid']}", - 'header' => { - 'Authorization' => @env['HTTP_AUTHORIZATION'] - }.compact - } - } - else - object['error'] = { - 'code' => 404, - 'message' => "Object does not exist on the server or you don't have permissions to access it", - } - end - end - - { 'objects' => all_objects } - end - - def upload_hypermedia_links(all_objects, existing_objects) - all_objects.each do |object| - # generate actions only for non-existing objects - next if existing_objects.include?(object['oid']) - - object['actions'] = { - 'upload' => { - 'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{object['oid']}/#{object['size']}", - 'header' => { - 'Authorization' => @env['HTTP_AUTHORIZATION'] - }.compact - } - } - end - - { 'objects' => all_objects } - end - end - end -end diff --git a/lib/gitlab/lfs/router.rb b/lib/gitlab/lfs/router.rb deleted file mode 100644 index f2a76a56b8f2452ea1a789e90feb20a548830060..0000000000000000000000000000000000000000 --- a/lib/gitlab/lfs/router.rb +++ /dev/null @@ -1,98 +0,0 @@ -module Gitlab - module Lfs - class Router - attr_reader :project, :user, :ci, :request - - def initialize(project, user, ci, request) - @project = project - @user = user - @ci = ci - @env = request.env - @request = request - end - - def try_call - return unless @request && @request.path.present? - - case @request.request_method - when 'GET' - get_response - when 'POST' - post_response - when 'PUT' - put_response - else - nil - end - end - - private - - def get_response - path_match = @request.path.match(/\/(info\/lfs|gitlab-lfs)\/objects\/([0-9a-f]{64})$/) - return nil unless path_match - - oid = path_match[2] - return nil unless oid - - case path_match[1] - when "info/lfs" - lfs.render_unsupported_deprecated_api - when "gitlab-lfs" - lfs.render_download_object_response(oid) - else - nil - end - end - - def post_response - post_path = @request.path.match(/\/info\/lfs\/objects(\/batch)?$/) - return nil unless post_path - - # Check for Batch API - if post_path[0].ends_with?("/info/lfs/objects/batch") - lfs.render_batch_operation_response - elsif post_path[0].ends_with?("/info/lfs/objects") - lfs.render_unsupported_deprecated_api - else - nil - end - end - - def put_response - object_match = @request.path.match(/\/gitlab-lfs\/objects\/([0-9a-f]{64})\/([0-9]+)(|\/authorize){1}$/) - return nil if object_match.nil? - - oid = object_match[1] - size = object_match[2].try(:to_i) - return nil if oid.nil? || size.nil? - - # GitLab-workhorse requests - # 1. Try to authorize the request - # 2. send a request with a header containing the name of the temporary file - if object_match[3] && object_match[3] == '/authorize' - lfs.render_storage_upload_authorize_response(oid, size) - else - tmp_file_name = sanitize_tmp_filename(@request.env['HTTP_X_GITLAB_LFS_TMP']) - lfs.render_storage_upload_store_response(oid, size, tmp_file_name) - end - end - - def lfs - return unless @project - - Gitlab::Lfs::Response.new(@project, @user, @ci, @request) - end - - def sanitize_tmp_filename(name) - if name.present? - name.gsub!(/^.*(\\|\/)/, '') - name = name.match(/[0-9a-f]{73}/) - name[0] if name - else - nil - end - end - end - end -end diff --git a/lib/gitlab/mail_room.rb b/lib/gitlab/mail_room.rb new file mode 100644 index 0000000000000000000000000000000000000000..12999a90a298fa78ef7c31ca5c9a1c6398199641 --- /dev/null +++ b/lib/gitlab/mail_room.rb @@ -0,0 +1,47 @@ +require 'yaml' +require 'json' +require_relative 'redis' unless defined?(Gitlab::Redis) + +module Gitlab + module MailRoom + class << self + def enabled? + config[:enabled] && config[:address] + end + + def config + @config ||= fetch_config + end + + def reset_config! + @config = nil + end + + private + + def fetch_config + return {} unless File.exist?(config_file) + + rails_env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development' + all_config = YAML.load_file(config_file)[rails_env].deep_symbolize_keys + + config = all_config[:incoming_email] || {} + config[:enabled] = false if config[:enabled].nil? + config[:port] = 143 if config[:port].nil? + config[:ssl] = false if config[:ssl].nil? + config[:start_tls] = false if config[:start_tls].nil? + config[:mailbox] = 'inbox' if config[:mailbox].nil? + + if config[:enabled] && config[:address] + config[:redis_url] = Gitlab::Redis.new(rails_env).url + end + + config + end + + def config_file + ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] || File.expand_path('../../../config/gitlab.yml', __FILE__) + end + end + end +end diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb index 1f92986ec9a09be9fa4b5b392951ca1e1b0b80aa..9376b54f43bc102cf1c5494659332adda38d515e 100644 --- a/lib/gitlab/redis.rb +++ b/lib/gitlab/redis.rb @@ -1,50 +1,94 @@ +# This file should not have any direct dependency on Rails environment +# please require all dependencies below: +require 'active_support/core_ext/hash/keys' + module Gitlab class Redis CACHE_NAMESPACE = 'cache:gitlab' SESSION_NAMESPACE = 'session:gitlab' SIDEKIQ_NAMESPACE = 'resque:gitlab' - - attr_reader :url + MAILROOM_NAMESPACE = 'mail_room:gitlab' + DEFAULT_REDIS_URL = 'redis://localhost:6379' # To be thread-safe we must be careful when writing the class instance # variables @url and @pool. Because @pool depends on @url we need two # mutexes to prevent deadlock. - URL_MUTEX = Mutex.new + PARAMS_MUTEX = Mutex.new POOL_MUTEX = Mutex.new - private_constant :URL_MUTEX, :POOL_MUTEX + private_constant :PARAMS_MUTEX, :POOL_MUTEX - def self.url - @url || URL_MUTEX.synchronize { @url = new.url } - end + class << self + def params + @params || PARAMS_MUTEX.synchronize { @params = new.params } + end + + # @deprecated Use .params instead to get sentinel support + def url + new.url + end - def self.with - if @pool.nil? - POOL_MUTEX.synchronize do - @pool = ConnectionPool.new { ::Redis.new(url: url) } + def with + if @pool.nil? + POOL_MUTEX.synchronize do + @pool = ConnectionPool.new { ::Redis.new(params) } + end end + @pool.with { |redis| yield redis } end - @pool.with { |redis| yield redis } + + def reset_params! + @params = nil + end + end + + def initialize(rails_env = nil) + @rails_env = rails_env || ::Rails.env end - def self.redis_store_options - url = new.url - redis_config_hash = ::Redis::Store::Factory.extract_host_options_from_uri(url) - # Redis::Store does not handle Unix sockets well, so let's do it for them - redis_uri = URI.parse(url) + def params + redis_store_options + end + + def url + raw_config_hash[:url] + end + + private + + def redis_store_options + config = raw_config_hash + redis_url = config.delete(:url) + redis_uri = URI.parse(redis_url) + if redis_uri.scheme == 'unix' - redis_config_hash[:path] = redis_uri.path + # Redis::Store does not handle Unix sockets well, so let's do it for them + config[:path] = redis_uri.path + config + else + redis_hash = ::Redis::Store::Factory.extract_host_options_from_uri(redis_url) + # order is important here, sentinels must be after the connection keys. + # {url: ..., port: ..., sentinels: [...]} + redis_hash.merge(config) end - redis_config_hash end - def initialize(rails_env = nil) - rails_env ||= Rails.env - config_file = File.expand_path('../../../config/resque.yml', __FILE__) + def raw_config_hash + config_data = fetch_config - @url = "redis://localhost:6379" - if File.exist?(config_file) - @url = YAML.load_file(config_file)[rails_env] + if config_data + config_data.is_a?(String) ? { url: config_data } : config_data.deep_symbolize_keys + else + { url: DEFAULT_REDIS_URL } end end + + def fetch_config + file = config_file + File.exist?(file) ? YAML.load_file(file)[@rails_env] : false + end + + def config_file + File.expand_path('../../../config/resque.yml', __FILE__) + end end end diff --git a/lib/gitlab/request_profiler/middleware.rb b/lib/gitlab/request_profiler/middleware.rb index 4e787dc0656c6888f9798a8a1ef17faabf321909..786e1d49f5e048da7fa2a79df2abdf3f36d314aa 100644 --- a/lib/gitlab/request_profiler/middleware.rb +++ b/lib/gitlab/request_profiler/middleware.rb @@ -1,5 +1,5 @@ require 'ruby-prof' -require 'gitlab/request_profiler' +require_dependency 'gitlab/request_profiler' module Gitlab module RequestProfiler diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 5166766954f522ad85f2604667dd739f993f98f8..a99ed3bd4a2e30ff052047e248e926abf540e9e5 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -47,7 +47,7 @@ namespace :gitlab do } correct_options = options.map do |name, value| - run(%W(#{Gitlab.config.git.bin_path} config --global --get #{name})).try(:squish) == value + run_command(%W(#{Gitlab.config.git.bin_path} config --global --get #{name})).try(:squish) == value end if correct_options.all? @@ -65,7 +65,7 @@ namespace :gitlab do for_more_information( see_installation_guide_section "GitLab" ) - end + end end end @@ -74,7 +74,7 @@ namespace :gitlab do database_config_file = Rails.root.join("config", "database.yml") - if File.exists?(database_config_file) + if File.exist?(database_config_file) puts "yes".color(:green) else puts "no".color(:red) @@ -95,7 +95,7 @@ namespace :gitlab do gitlab_config_file = Rails.root.join("config", "gitlab.yml") - if File.exists?(gitlab_config_file) + if File.exist?(gitlab_config_file) puts "yes".color(:green) else puts "no".color(:red) @@ -114,7 +114,7 @@ namespace :gitlab do print "GitLab config outdated? ... " gitlab_config_file = Rails.root.join("config", "gitlab.yml") - unless File.exists?(gitlab_config_file) + unless File.exist?(gitlab_config_file) puts "can't check because of previous errors".color(:magenta) end @@ -145,7 +145,7 @@ namespace :gitlab do script_path = "/etc/init.d/gitlab" - if File.exists?(script_path) + if File.exist?(script_path) puts "yes".color(:green) else puts "no".color(:red) @@ -170,7 +170,7 @@ namespace :gitlab do recipe_path = Rails.root.join("lib/support/init.d/", "gitlab") script_path = "/etc/init.d/gitlab" - unless File.exists?(script_path) + unless File.exist?(script_path) puts "can't check because of previous errors".color(:magenta) return end @@ -317,7 +317,7 @@ namespace :gitlab do min_redis_version = "2.8.0" print "Redis version >= #{min_redis_version}? ... " - redis_version = run(%W(redis-cli --version)) + redis_version = run_command(%W(redis-cli --version)) redis_version = redis_version.try(:match, /redis-cli (\d+\.\d+\.\d+)/) if redis_version && (Gem::Version.new(redis_version[1]) > Gem::Version.new(min_redis_version)) @@ -362,7 +362,7 @@ namespace :gitlab do Gitlab.config.repositories.storages.each do |name, repo_base_path| print "#{name}... " - if File.exists?(repo_base_path) + if File.exist?(repo_base_path) puts "yes".color(:green) else puts "no".color(:red) @@ -386,7 +386,7 @@ namespace :gitlab do Gitlab.config.repositories.storages.each do |name, repo_base_path| print "#{name}... " - unless File.exists?(repo_base_path) + unless File.exist?(repo_base_path) puts "can't check because of previous errors".color(:magenta) return end @@ -409,7 +409,7 @@ namespace :gitlab do Gitlab.config.repositories.storages.each do |name, repo_base_path| print "#{name}... " - unless File.exists?(repo_base_path) + unless File.exist?(repo_base_path) puts "can't check because of previous errors".color(:magenta) return end @@ -439,7 +439,7 @@ namespace :gitlab do Gitlab.config.repositories.storages.each do |name, repo_base_path| print "#{name}... " - unless File.exists?(repo_base_path) + unless File.exist?(repo_base_path) puts "can't check because of previous errors".color(:magenta) return end @@ -894,7 +894,7 @@ namespace :gitlab do def check_ruby_version required_version = Gitlab::VersionInfo.new(2, 1, 0) - current_version = Gitlab::VersionInfo.parse(run(%W(ruby --version))) + current_version = Gitlab::VersionInfo.parse(run_command(%W(ruby --version))) print "Ruby version >= #{required_version} ? ... " @@ -911,7 +911,7 @@ namespace :gitlab do def check_git_version required_version = Gitlab::VersionInfo.new(2, 7, 3) - current_version = Gitlab::VersionInfo.parse(run(%W(#{Gitlab.config.git.bin_path} --version))) + current_version = Gitlab::VersionInfo.parse(run_command(%W(#{Gitlab.config.git.bin_path} --version))) puts "Your git bin path is \"#{Gitlab.config.git.bin_path}\"" print "Git version >= #{required_version} ? ... " diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake index 5ca73f58d72c91b0477114834bdff0b524022e10..2e1558664745317fae6dd2fc1f07e5a0510a798c 100644 --- a/lib/tasks/gitlab/info.rake +++ b/lib/tasks/gitlab/info.rake @@ -8,7 +8,7 @@ namespace :gitlab do # check Ruby version ruby_version = run_and_match(%W(ruby --version), /[\d\.p]+/).try(:to_s) # check Gem version - gem_version = run(%W(gem --version)) + gem_version = run_command(%W(gem --version)) # check Bundler version bunder_version = run_and_match(%W(bundle --version), /[\d\.]+/).try(:to_s) # check Bundler version @@ -17,7 +17,7 @@ namespace :gitlab do puts "" puts "System information".color(:yellow) puts "System:\t\t#{os_name || "unknown".color(:red)}" - puts "Current User:\t#{run(%W(whoami))}" + puts "Current User:\t#{run_command(%W(whoami))}" puts "Using RVM:\t#{rvm_version.present? ? "yes".color(:green) : "no"}" puts "RVM Version:\t#{rvm_version}" if rvm_version.present? puts "Ruby Version:\t#{ruby_version || "unknown".color(:red)}" diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake index ba93945bd031874cc642842648a67243c06c31c1..bb7eb852f1b8603e7a6aced3ce550edebf86e2ea 100644 --- a/lib/tasks/gitlab/shell.rake +++ b/lib/tasks/gitlab/shell.rake @@ -90,7 +90,7 @@ namespace :gitlab do task build_missing_projects: :environment do Project.find_each(batch_size: 1000) do |project| path_to_repo = project.repository.path_to_repo - if File.exists?(path_to_repo) + if File.exist?(path_to_repo) print '-' else if Gitlab::Shell.new.add_repository(project.repository_storage_path, diff --git a/lib/tasks/gitlab/task_helpers.rake b/lib/tasks/gitlab/task_helpers.rake index ab96b1d35932c9802787ef65e6db2f189961993e..74be413423aa9ece819d105ff0d4b09d153ac63a 100644 --- a/lib/tasks/gitlab/task_helpers.rake +++ b/lib/tasks/gitlab/task_helpers.rake @@ -23,7 +23,7 @@ namespace :gitlab do # It will primarily use lsb_relase to determine the OS. # It has fallbacks to Debian, SuSE, OS X and systems running systemd. def os_name - os_name = run(%W(lsb_release -irs)) + os_name = run_command(%W(lsb_release -irs)) os_name ||= if File.readable?('/etc/system-release') File.read('/etc/system-release') end @@ -34,7 +34,7 @@ namespace :gitlab do os_name ||= if File.readable?('/etc/SuSE-release') File.read('/etc/SuSE-release') end - os_name ||= if os_x_version = run(%W(sw_vers -productVersion)) + os_name ||= if os_x_version = run_command(%W(sw_vers -productVersion)) "Mac OS X #{os_x_version}" end os_name ||= if File.readable?('/etc/os-release') @@ -62,10 +62,10 @@ namespace :gitlab do # Returns nil if nothing matched # Returns the MatchData if the pattern matched # - # see also #run + # see also #run_command # see also String#match def run_and_match(command, regexp) - run(command).try(:match, regexp) + run_command(command).try(:match, regexp) end # Runs the given command @@ -74,7 +74,7 @@ namespace :gitlab do # Returns the output of the command otherwise # # see also #run_and_match - def run(command) + def run_command(command) output, _ = Gitlab::Popen.popen(command) output rescue Errno::ENOENT @@ -82,7 +82,7 @@ namespace :gitlab do end def uid_for(user_name) - run(%W(id -u #{user_name})).chomp.to_i + run_command(%W(id -u #{user_name})).chomp.to_i end def gid_for(group_name) @@ -96,7 +96,7 @@ namespace :gitlab do def warn_user_is_not_gitlab unless @warned_user_not_gitlab gitlab_user = Gitlab.config.gitlab.user - current_user = run(%W(whoami)).chomp + current_user = run_command(%W(whoami)).chomp unless current_user == gitlab_user puts " Warning ".color(:black).background(:yellow) puts " You are running as user #{current_user.color(:magenta)}, we hope you know what you are doing." diff --git a/lib/tasks/spinach.rake b/lib/tasks/spinach.rake index da255f5464b04f77be7b1cca3d95904f9102b989..8dbfa7751dcf252c4f7541091c5fffce6e38a470 100644 --- a/lib/tasks/spinach.rake +++ b/lib/tasks/spinach.rake @@ -34,21 +34,19 @@ task :spinach do run_spinach_tests(nil) end -def run_command(cmd) +def run_system_command(cmd) system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) end def run_spinach_command(args) - run_command(%w(spinach -r rerun) + args) + run_system_command(%w(spinach -r rerun) + args) end def run_spinach_tests(tags) - #run_command(%w(rake gitlab:setup)) or raise('gitlab:setup failed!') - success = run_spinach_command(%W(--tags #{tags})) 3.times do |_| break if success - break unless File.exists?('tmp/spinach-rerun.txt') + break unless File.exist?('tmp/spinach-rerun.txt') tests = File.foreach('tmp/spinach-rerun.txt').map(&:chomp) puts '' diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh new file mode 100755 index 0000000000000000000000000000000000000000..bc6e4d940611423a83dc9b664099dc11b4c1fa99 --- /dev/null +++ b/scripts/lint-doc.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +cd "$(dirname "$0")/.." + +# Use long options (e.g. --header instead of -H) for curl examples in documentation. +grep --perl-regexp --recursive --color=auto 'curl (.+ )?-[^- ].*' doc/ +if [ $? == 0 ] +then + echo '✖ ERROR: Short options should not be used in documentation!' >&2 + exit 1 +fi + +echo "✔ Linting passed" +exit 0 + diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb index 6fad7e2b9e7700b493f01f4b87d29c5c62b89d73..c5d3cd70acc87deddf951ec56f26d9cb9cd2f7c8 100644 --- a/spec/config/mail_room_spec.rb +++ b/spec/config/mail_room_spec.rb @@ -1,53 +1,48 @@ -require "spec_helper" +require 'spec_helper' -describe "mail_room.yml" do - let(:config_path) { "config/mail_room.yml" } +describe 'mail_room.yml' do + let(:config_path) { 'config/mail_room.yml' } let(:configuration) { YAML.load(ERB.new(File.read(config_path)).result) } - context "when incoming email is disabled" do + context 'when incoming email is disabled' do before do - ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = Rails.root.join("spec/fixtures/mail_room_disabled.yml").to_s + ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/mail_room_disabled.yml').to_s + Gitlab::MailRoom.reset_config! end after do - ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = nil + ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = nil end - it "contains no configuration" do + it 'contains no configuration' do expect(configuration[:mailboxes]).to be_nil end end - context "when incoming email is enabled" do + context 'when incoming email is enabled' do before do - ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = Rails.root.join("spec/fixtures/mail_room_enabled.yml").to_s + ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/mail_room_enabled.yml').to_s + Gitlab::MailRoom.reset_config! end after do - ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = nil + ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = nil end - it "contains the intended configuration" do + it 'contains the intended configuration' do expect(configuration[:mailboxes].length).to eq(1) mailbox = configuration[:mailboxes].first - expect(mailbox[:host]).to eq("imap.gmail.com") + expect(mailbox[:host]).to eq('imap.gmail.com') expect(mailbox[:port]).to eq(993) expect(mailbox[:ssl]).to eq(true) expect(mailbox[:start_tls]).to eq(false) - expect(mailbox[:email]).to eq("gitlab-incoming@gmail.com") - expect(mailbox[:password]).to eq("[REDACTED]") - expect(mailbox[:name]).to eq("inbox") - - redis_config_file = Rails.root.join('config', 'resque.yml') - - redis_url = - if File.exist?(redis_config_file) - YAML.load_file(redis_config_file)[Rails.env] - else - "redis://localhost:6379" - end + expect(mailbox[:email]).to eq('gitlab-incoming@gmail.com') + expect(mailbox[:password]).to eq('[REDACTED]') + expect(mailbox[:name]).to eq('inbox') + + redis_url = Gitlab::Redis.url expect(mailbox[:delivery_options][:redis_url]).to eq(redis_url) expect(mailbox[:arbitration_options][:redis_url]).to eq(redis_url) diff --git a/spec/factories/broadcast_messages.rb b/spec/factories/broadcast_messages.rb index efe9803b1a7159fc40debcb74442d7b51880fa17..c2fdf89213a614377edc126ae36c5494cfcd4a24 100644 --- a/spec/factories/broadcast_messages.rb +++ b/spec/factories/broadcast_messages.rb @@ -1,8 +1,8 @@ FactoryGirl.define do factory :broadcast_message do message "MyText" - starts_at Date.yesterday - ends_at Date.tomorrow + starts_at 1.day.ago + ends_at 1.day.from_now trait :expired do starts_at 5.days.ago diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 1b32d560b1620e4fb199235570202e5eef6e4a51..0c93bbdfe26ebfb2a0ca980f86700a93aaff09e5 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -7,6 +7,7 @@ stage_idx 0 ref 'master' tag false + status 'pending' created_at 'Di 29. Okt 09:50:00 CET 2013' started_at 'Di 29. Okt 09:51:28 CET 2013' finished_at 'Di 29. Okt 09:53:28 CET 2013' @@ -45,6 +46,10 @@ status 'pending' end + trait :created do + status 'created' + end + trait :manual do status 'skipped' self.when 'manual' diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb index a039bef6f3c44ec1751c8566713b0d76f5d63e12..04d66020c87b74a8359c47ca14bd97e77a65e1a8 100644 --- a/spec/factories/ci/pipelines.rb +++ b/spec/factories/ci/pipelines.rb @@ -18,7 +18,9 @@ FactoryGirl.define do factory :ci_empty_pipeline, class: Ci::Pipeline do + ref 'master' sha '97de212e80737a608d939f648d959671fb0a0142' + status 'pending' project factory: :empty_project diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb index 1e5c479616c50b0531cee190035f85d8bbd51757..995f2080f1008e05497b639ec0267972bc90c8f7 100644 --- a/spec/factories/commit_statuses.rb +++ b/spec/factories/commit_statuses.rb @@ -7,6 +7,30 @@ started_at 'Tue, 26 Jan 2016 08:21:42 +0100' finished_at 'Tue, 26 Jan 2016 08:23:42 +0100' + trait :success do + status 'success' + end + + trait :failed do + status 'failed' + end + + trait :canceled do + status 'canceled' + end + + trait :running do + status 'running' + end + + trait :pending do + status 'pending' + end + + trait :created do + status 'created' + end + after(:build) do |build, evaluator| build.project = build.pipeline.project end diff --git a/spec/features/admin/admin_disables_git_access_protocol_spec.rb b/spec/features/admin/admin_disables_git_access_protocol_spec.rb index 5b1c0460274087b2b7f3fab5ba8849d356c4e39e..66044b444952de468dd1448ae61bbd47d8fcca89 100644 --- a/spec/features/admin/admin_disables_git_access_protocol_spec.rb +++ b/spec/features/admin/admin_disables_git_access_protocol_spec.rb @@ -45,7 +45,6 @@ expect(page).to have_content("git clone #{project.ssh_url_to_repo}") expect(page).to have_selector('#clone-dropdown') end - end def visit_project diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb index f676200ecf33631f3499e97f3896abe0e431d8d9..4d5d4aa121add23e76759dc7de4fd0f318ac3001 100644 --- a/spec/features/merge_requests/created_from_fork_spec.rb +++ b/spec/features/merge_requests/created_from_fork_spec.rb @@ -29,12 +29,16 @@ include WaitForAjax given(:pipeline) do - create(:ci_pipeline_with_two_job, project: fork_project, - sha: merge_request.diff_head_sha, - ref: merge_request.source_branch) + create(:ci_pipeline, + project: fork_project, + sha: merge_request.diff_head_sha, + ref: merge_request.source_branch) end - background { pipeline.create_builds(user) } + background do + create(:ci_build, pipeline: pipeline, name: 'rspec') + create(:ci_build, pipeline: pipeline, name: 'spinach') + end scenario 'user visits a pipelines page', js: true do visit_merge_request(merge_request) diff --git a/spec/features/merge_requests/edit_mr_spec.rb b/spec/features/merge_requests/edit_mr_spec.rb index 2468933e7f7955183e863de09083dbd15b0eb2dc..48c121bb98a7b47cba3602c8faddf7c9fe6c1ff3 100644 --- a/spec/features/merge_requests/edit_mr_spec.rb +++ b/spec/features/merge_requests/edit_mr_spec.rb @@ -14,7 +14,7 @@ visit edit_namespace_project_merge_request_path(project.namespace, project, merge_request) end - it 'form has class js-quick-submit' do + it 'has class js-quick-submit in form' do expect(page).to have_selector('.js-quick-submit') end end diff --git a/spec/features/profiles/password_spec.rb b/spec/features/profiles/password_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..4cbdd89d46f02830d5cfc8e1ea5248fe373e4973 --- /dev/null +++ b/spec/features/profiles/password_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe 'Profile > Password', feature: true do + let(:user) { create(:user, password_automatically_set: true) } + + before do + login_as(user) + visit edit_profile_password_path + end + + def fill_passwords(password, confirmation) + fill_in 'New password', with: password + fill_in 'Password confirmation', with: confirmation + + click_button 'Save password' + end + + context 'User with password automatically set' do + describe 'User puts different passwords in the field and in the confirmation' do + it 'shows an error message' do + fill_passwords('mypassword', 'mypassword2') + + page.within('.alert-danger') do + expect(page).to have_content("Password confirmation doesn't match Password") + end + end + + it 'does not contains the current password field after an error' do + fill_passwords('mypassword', 'mypassword2') + + expect(page).to have_no_field('user[current_password]') + end + end + + describe 'User puts the same passwords in the field and in the confirmation' do + it 'shows a success message' do + fill_passwords('mypassword', 'mypassword') + + page.within('.flash-notice') do + expect(page).to have_content('Password was successfully updated. Please login with it') + end + end + end + end +end diff --git a/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..10b91d8990b823c076f90b2c67f866ebf8997937 --- /dev/null +++ b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +feature 'User views files page', feature: true do + include WaitForAjax + + let(:user) { create(:user) } + let(:project) { create(:forked_project_with_submodules) } + + before do + project.team << [user, :master] + login_as user + visit namespace_project_tree_path(project.namespace, project, project.repository.root_ref) + end + + scenario 'user sees folders and submodules sorted together, followed by files' do + rows = all('td.tree-item-file-name').map(&:text) + tree = project.repository.tree + + folders = tree.trees.map(&:name) + files = tree.blobs.map(&:name) + submodules = tree.submodules.map do |submodule| + submodule.name + " @ " + submodule.id[0..7] + end + + sorted_titles = (folders + submodules).sort + files + + expect(rows).to eq(sorted_titles) + end +end diff --git a/spec/features/pipelines_spec.rb b/spec/features/projects/pipelines_spec.rb similarity index 78% rename from spec/features/pipelines_spec.rb rename to spec/features/projects/pipelines_spec.rb index eace76c370f15ccccc0018afaceb9a4dd1333f98..29d150bc5971be934b218e04c53774e7fa6c6a55 100644 --- a/spec/features/pipelines_spec.rb +++ b/spec/features/projects/pipelines_spec.rb @@ -12,7 +12,7 @@ end describe 'GET /:project/pipelines' do - let!(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', status: 'running') } + let!(:pipeline) { create(:ci_empty_pipeline, project: project, ref: 'master', status: 'running') } [:all, :running, :branches].each do |scope| context "displaying #{scope}" do @@ -31,9 +31,12 @@ end context 'cancelable pipeline' do - let!(:running) { create(:ci_build, :running, pipeline: pipeline, stage: 'test', commands: 'test') } + let!(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') } - before { visit namespace_project_pipelines_path(project.namespace, project) } + before do + build.run + visit namespace_project_pipelines_path(project.namespace, project) + end it { expect(page).to have_link('Cancel') } it { expect(page).to have_selector('.ci-running') } @@ -47,9 +50,12 @@ end context 'retryable pipelines' do - let!(:failed) { create(:ci_build, :failed, pipeline: pipeline, stage: 'test', commands: 'test') } + let!(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') } - before { visit namespace_project_pipelines_path(project.namespace, project) } + before do + build.drop + visit namespace_project_pipelines_path(project.namespace, project) + end it { expect(page).to have_link('Retry') } it { expect(page).to have_selector('.ci-failed') } @@ -58,7 +64,7 @@ before { click_link('Retry') } it { expect(page).not_to have_link('Retry') } - it { expect(page).to have_selector('.ci-pending') } + it { expect(page).to have_selector('.ci-running') } end end @@ -80,7 +86,9 @@ context 'when running' do let!(:running) { create(:generic_commit_status, status: 'running', pipeline: pipeline, stage: 'test') } - before { visit namespace_project_pipelines_path(project.namespace, project) } + before do + visit namespace_project_pipelines_path(project.namespace, project) + end it 'is not cancelable' do expect(page).not_to have_link('Cancel') @@ -92,9 +100,12 @@ end context 'when failed' do - let!(:running) { create(:generic_commit_status, status: 'failed', pipeline: pipeline, stage: 'test') } + let!(:status) { create(:generic_commit_status, :pending, pipeline: pipeline, stage: 'test') } - before { visit namespace_project_pipelines_path(project.namespace, project) } + before do + status.drop + visit namespace_project_pipelines_path(project.namespace, project) + end it 'is not retryable' do expect(page).not_to have_link('Retry') @@ -194,7 +205,7 @@ before { visit new_namespace_project_pipeline_path(project.namespace, project) } context 'for valid commit' do - before { fill_in('Create for', with: 'master') } + before { fill_in('pipeline[ref]', with: 'master') } context 'with gitlab-ci.yml' do before { stub_ci_pipeline_to_return_yaml_file } @@ -211,11 +222,37 @@ context 'for invalid commit' do before do - fill_in('Create for', with: 'invalid reference') + fill_in('pipeline[ref]', with: 'invalid-reference') click_on 'Create pipeline' end it { expect(page).to have_content('Reference not found') } end end + + describe 'Create pipelines', feature: true do + let(:project) { create(:project) } + + before do + visit new_namespace_project_pipeline_path(project.namespace, project) + end + + describe 'new pipeline page' do + it 'has field to add a new pipeline' do + expect(page).to have_field('pipeline[ref]') + expect(page).to have_content('Create for') + end + end + + describe 'find pipelines' do + it 'shows filtered pipelines', js: true do + fill_in('pipeline[ref]', with: 'fix') + find('input#ref').native.send_keys(:keydown) + + within('.ui-autocomplete') do + expect(page).to have_selector('li', text: 'fix') + end + end + end + end end diff --git a/spec/fixtures/config/redis_new_format_host.yml b/spec/fixtures/config/redis_new_format_host.yml new file mode 100644 index 0000000000000000000000000000000000000000..13772677a45c2e837656e065ca272baed8858aac --- /dev/null +++ b/spec/fixtures/config/redis_new_format_host.yml @@ -0,0 +1,29 @@ +# redis://[:password@]host[:port][/db-number][?option=value] +# more details: http://www.iana.org/assignments/uri-schemes/prov/redis +development: + url: redis://:mynewpassword@localhost:6379/99 + sentinels: + - + host: localhost + port: 26380 # point to sentinel, not to redis port + - + host: slave2 + port: 26381 # point to sentinel, not to redis port +test: + url: redis://:mynewpassword@localhost:6379/99 + sentinels: + - + host: localhost + port: 26380 # point to sentinel, not to redis port + - + host: slave2 + port: 26381 # point to sentinel, not to redis port +production: + url: redis://:mynewpassword@localhost:6379/99 + sentinels: + - + host: slave1 + port: 26380 # point to sentinel, not to redis port + - + host: slave2 + port: 26381 # point to sentinel, not to redis port diff --git a/spec/fixtures/config/redis_new_format_socket.yml b/spec/fixtures/config/redis_new_format_socket.yml new file mode 100644 index 0000000000000000000000000000000000000000..4e76830c281de7a02d15d74e913ec4c5fb998b7d --- /dev/null +++ b/spec/fixtures/config/redis_new_format_socket.yml @@ -0,0 +1,6 @@ +development: + url: unix:/path/to/redis.sock +test: + url: unix:/path/to/redis.sock +production: + url: unix:/path/to/redis.sock diff --git a/spec/fixtures/config/redis_old_format_host.yml b/spec/fixtures/config/redis_old_format_host.yml new file mode 100644 index 0000000000000000000000000000000000000000..253d0a994f5e6f12e515876051d4388e5d300917 --- /dev/null +++ b/spec/fixtures/config/redis_old_format_host.yml @@ -0,0 +1,5 @@ +# redis://[:password@]host[:port][/db-number][?option=value] +# more details: http://www.iana.org/assignments/uri-schemes/prov/redis +development: redis://:mypassword@localhost:6379/99 +test: redis://:mypassword@localhost:6379/99 +production: redis://:mypassword@localhost:6379/99 diff --git a/spec/fixtures/config/redis_old_format_socket.yml b/spec/fixtures/config/redis_old_format_socket.yml new file mode 100644 index 0000000000000000000000000000000000000000..fd31ce8ea3d77ff714ccfd3324e4d883ee6501a7 --- /dev/null +++ b/spec/fixtures/config/redis_old_format_socket.yml @@ -0,0 +1,3 @@ +development: unix:/path/to/old/redis.sock +test: unix:/path/to/old/redis.sock +production: unix:/path/to/old/redis.sock diff --git a/spec/fixtures/project_services/campfire/rooms.json b/spec/fixtures/project_services/campfire/rooms.json new file mode 100644 index 0000000000000000000000000000000000000000..71e9645c955f8838b189bdc836fdbd332793e618 --- /dev/null +++ b/spec/fixtures/project_services/campfire/rooms.json @@ -0,0 +1,22 @@ +{ + "rooms": [ + { + "name": "test-room", + "locked": false, + "created_at": "2009/01/07 20:43:11 +0000", + "updated_at": "2009/03/18 14:31:39 +0000", + "topic": "The room topic\n", + "id": 123, + "membership_limit": 4 + }, + { + "name": "another room", + "locked": true, + "created_at": "2009/03/18 14:30:42 +0000", + "updated_at": "2013/01/27 14:14:27 +0000", + "topic": "Comment, ideas, GitHub notifications for eCommittee App", + "id": 456, + "membership_limit": 4 + } + ] +} diff --git a/spec/fixtures/project_services/campfire/rooms2.json b/spec/fixtures/project_services/campfire/rooms2.json new file mode 100644 index 0000000000000000000000000000000000000000..3d5f635d8b39a8d88dc0764ba85259604bc0c41a --- /dev/null +++ b/spec/fixtures/project_services/campfire/rooms2.json @@ -0,0 +1,22 @@ +{ + "rooms": [ + { + "name": "test-room-not-found", + "locked": false, + "created_at": "2009/01/07 20:43:11 +0000", + "updated_at": "2009/03/18 14:31:39 +0000", + "topic": "The room topic\n", + "id": 123, + "membership_limit": 4 + }, + { + "name": "another room", + "locked": true, + "created_at": "2009/03/18 14:30:42 +0000", + "updated_at": "2013/01/27 14:14:27 +0000", + "topic": "Comment, ideas, GitHub notifications for eCommittee App", + "id": 456, + "membership_limit": 4 + } + ] +} diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb index f75fdb739f6891b0f547928043cc2210e19f30d0..7998209b7b00e7a759eade60dfa2e42ed37e7990 100644 --- a/spec/helpers/members_helper_spec.rb +++ b/spec/helpers/members_helper_spec.rb @@ -9,54 +9,6 @@ it { expect(action_member_permission(:admin, group_member)).to eq :admin_group_member } end - describe '#default_show_roles' do - let(:user) { double } - let(:member) { build(:project_member) } - - before do - allow(helper).to receive(:current_user).and_return(user) - allow(helper).to receive(:can?).with(user, :update_project_member, member).and_return(false) - allow(helper).to receive(:can?).with(user, :destroy_project_member, member).and_return(false) - allow(helper).to receive(:can?).with(user, :admin_project_member, member.source).and_return(false) - end - - context 'when the current cannot update, destroy or admin the passed member' do - it 'returns false' do - expect(helper.default_show_roles(member)).to be_falsy - end - end - - context 'when the current can update the passed member' do - before do - allow(helper).to receive(:can?).with(user, :update_project_member, member).and_return(true) - end - - it 'returns true' do - expect(helper.default_show_roles(member)).to be_truthy - end - end - - context 'when the current can destroy the passed member' do - before do - allow(helper).to receive(:can?).with(user, :destroy_project_member, member).and_return(true) - end - - it 'returns true' do - expect(helper.default_show_roles(member)).to be_truthy - end - end - - context 'when the current can admin the passed member source' do - before do - allow(helper).to receive(:can?).with(user, :admin_project_member, member.source).and_return(true) - end - - it 'returns true' do - expect(helper.default_show_roles(member)).to be_truthy - end - end - end - describe '#remove_member_message' do let(:requester) { build(:user) } let(:project) { create(:project) } diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..837b0de9a4cba692e5127759940706926251200c --- /dev/null +++ b/spec/initializers/secret_token_spec.rb @@ -0,0 +1,200 @@ +require 'spec_helper' +require_relative '../../config/initializers/secret_token' + +describe 'create_tokens', lib: true do + let(:secrets) { ActiveSupport::OrderedOptions.new } + + before do + allow(ENV).to receive(:[]).and_call_original + allow(File).to receive(:write) + allow(File).to receive(:delete) + allow(Rails).to receive_message_chain(:application, :secrets).and_return(secrets) + allow(Rails).to receive_message_chain(:root, :join) { |string| string } + allow(self).to receive(:warn) + allow(self).to receive(:exit) + end + + context 'setting secret_key_base and otp_key_base' do + context 'when none of the secrets exist' do + before do + allow(ENV).to receive(:[]).with('SECRET_KEY_BASE').and_return(nil) + allow(File).to receive(:exist?).with('.secret').and_return(false) + allow(File).to receive(:exist?).with('config/secrets.yml').and_return(false) + allow(self).to receive(:warn_missing_secret) + end + + it 'generates different secrets for secret_key_base, otp_key_base, and db_key_base' do + create_tokens + + keys = secrets.values_at(:secret_key_base, :otp_key_base, :db_key_base) + + expect(keys.uniq).to eq(keys) + expect(keys.map(&:length)).to all(eq(128)) + end + + it 'warns about the secrets to add to secrets.yml' do + expect(self).to receive(:warn_missing_secret).with('secret_key_base') + expect(self).to receive(:warn_missing_secret).with('otp_key_base') + expect(self).to receive(:warn_missing_secret).with('db_key_base') + + create_tokens + end + + it 'writes the secrets to secrets.yml' do + expect(File).to receive(:write).with('config/secrets.yml', any_args) do |filename, contents, options| + new_secrets = YAML.load(contents)[Rails.env] + + expect(new_secrets['secret_key_base']).to eq(secrets.secret_key_base) + expect(new_secrets['otp_key_base']).to eq(secrets.otp_key_base) + expect(new_secrets['db_key_base']).to eq(secrets.db_key_base) + end + + create_tokens + end + + it 'does not write a .secret file' do + expect(File).not_to receive(:write).with('.secret') + + create_tokens + end + end + + context 'when the other secrets all exist' do + before do + secrets.db_key_base = 'db_key_base' + + allow(File).to receive(:exist?).with('.secret').and_return(true) + allow(File).to receive(:read).with('.secret').and_return('file_key') + end + + context 'when secret_key_base exists in the environment and secrets.yml' do + before do + allow(ENV).to receive(:[]).with('SECRET_KEY_BASE').and_return('env_key') + secrets.secret_key_base = 'secret_key_base' + secrets.otp_key_base = 'otp_key_base' + end + + it 'does not issue a warning' do + expect(self).not_to receive(:warn) + + create_tokens + end + + it 'uses the environment variable' do + create_tokens + + expect(secrets.secret_key_base).to eq('env_key') + end + + it 'does not update secrets.yml' do + expect(File).not_to receive(:write) + + create_tokens + end + end + + context 'when secret_key_base and otp_key_base exist' do + before do + secrets.secret_key_base = 'secret_key_base' + secrets.otp_key_base = 'otp_key_base' + end + + it 'does not write any files' do + expect(File).not_to receive(:write) + + create_tokens + end + + it 'sets the the keys to the values from the environment and secrets.yml' do + create_tokens + + expect(secrets.secret_key_base).to eq('secret_key_base') + expect(secrets.otp_key_base).to eq('otp_key_base') + expect(secrets.db_key_base).to eq('db_key_base') + end + + it 'deletes the .secret file' do + expect(File).to receive(:delete).with('.secret') + + create_tokens + end + end + + context 'when secret_key_base and otp_key_base do not exist' do + before do + allow(File).to receive(:exist?).with('config/secrets.yml').and_return(true) + allow(YAML).to receive(:load_file).with('config/secrets.yml').and_return('test' => secrets.to_h.stringify_keys) + allow(self).to receive(:warn_missing_secret) + end + + it 'uses the file secret' do + expect(File).to receive(:write) do |filename, contents, options| + new_secrets = YAML.load(contents)[Rails.env] + + expect(new_secrets['secret_key_base']).to eq('file_key') + expect(new_secrets['otp_key_base']).to eq('file_key') + expect(new_secrets['db_key_base']).to eq('db_key_base') + end + + create_tokens + + expect(secrets.otp_key_base).to eq('file_key') + end + + it 'keeps the other secrets as they were' do + create_tokens + + expect(secrets.db_key_base).to eq('db_key_base') + end + + it 'warns about the missing secrets' do + expect(self).to receive(:warn_missing_secret).with('secret_key_base') + expect(self).to receive(:warn_missing_secret).with('otp_key_base') + + create_tokens + end + + it 'deletes the .secret file' do + expect(File).to receive(:delete).with('.secret') + + create_tokens + end + end + end + + context 'when db_key_base is blank but exists in secrets.yml' do + before do + secrets.otp_key_base = 'otp_key_base' + secrets.secret_key_base = 'secret_key_base' + yaml_secrets = secrets.to_h.stringify_keys.merge('db_key_base' => '<%= an_erb_expression %>') + + allow(File).to receive(:exist?).with('.secret').and_return(false) + allow(File).to receive(:exist?).with('config/secrets.yml').and_return(true) + allow(YAML).to receive(:load_file).with('config/secrets.yml').and_return('test' => yaml_secrets) + allow(self).to receive(:warn_missing_secret) + end + + it 'warns about updating db_key_base' do + expect(self).to receive(:warn_missing_secret).with('db_key_base') + + create_tokens + end + + it 'warns about the blank value existing in secrets.yml and exits' do + expect(self).to receive(:warn) do |warning| + expect(warning).to include('db_key_base') + expect(warning).to include('<%= an_erb_expression %>') + end + + create_tokens + end + + it 'does not update secrets.yml' do + expect(self).to receive(:exit).with(1).and_call_original + expect(File).not_to receive(:write) + + expect { create_tokens }.to raise_error(SystemExit) + end + end + end +end diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb index bda8d2ce38a3dc9e80f36df0e7b9c5adf154639c..6b58f3e43ee1153f91461873052cfcd1a235d10d 100644 --- a/spec/lib/banzai/filter/relative_link_filter_spec.rb +++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb @@ -3,7 +3,7 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do def filter(doc, contexts = {}) contexts.reverse_merge!({ - commit: project.commit, + commit: commit, project: project, project_wiki: project_wiki, ref: ref, @@ -28,6 +28,7 @@ def link(path) let(:project) { create(:project) } let(:project_path) { project.path_with_namespace } let(:ref) { 'markdown' } + let(:commit) { project.commit(ref) } let(:project_wiki) { nil } let(:requested_path) { '/' } @@ -77,7 +78,13 @@ def link(path) expect { filter(act) }.not_to raise_error end - context 'with a valid repository' do + it 'ignores ref if commit is passed' do + doc = filter(link('non/existent.file'), commit: project.commit('empty-branch') ) + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/#{ref}/non/existent.file" # non-existent files have no leading blob/raw/tree + end + + shared_examples :valid_repository do it 'rebuilds absolute URL for a file in the repo' do doc = filter(link('/doc/api/README.md')) expect(doc.at_css('a')['href']). @@ -189,4 +196,13 @@ def link(path) include_examples :relative_to_requested end end + + context 'with a valid commit' do + include_examples :valid_repository + end + + context 'with a valid ref' do + let(:commit) { nil } # force filter to use ref instead of commit + include_examples :valid_repository + end end diff --git a/spec/lib/banzai/filter/video_link_filter_spec.rb b/spec/lib/banzai/filter/video_link_filter_spec.rb index cc4349f80ba5aa18f2f60c44b53afd3b37062d1b..6ab1be9ccb70e879d0c23369cf1c2bd7812411e1 100644 --- a/spec/lib/banzai/filter/video_link_filter_spec.rb +++ b/spec/lib/banzai/filter/video_link_filter_spec.rb @@ -47,5 +47,4 @@ def link_to_image(path) expect(element['src']).to eq '/path/my_image.jpg' end end - end diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/ci/charts_spec.rb index 034ea098193adf5458de21dccdc2f1e8cb26f430..fb6cc398307807511dfb7e4d9fbba836f56aead5 100644 --- a/spec/lib/ci/charts_spec.rb +++ b/spec/lib/ci/charts_spec.rb @@ -2,21 +2,23 @@ describe Ci::Charts, lib: true do context "build_times" do + let(:project) { create(:empty_project) } + let(:chart) { Ci::Charts::BuildTime.new(project) } + + subject { chart.build_times } + before do - @pipeline = FactoryGirl.create(:ci_pipeline) - FactoryGirl.create(:ci_build, pipeline: @pipeline) + create(:ci_empty_pipeline, project: project, duration: 120) end it 'returns build times in minutes' do - chart = Ci::Charts::BuildTime.new(@pipeline.project) - expect(chart.build_times).to eq([2]) + is_expected.to contain_exactly(2) end it 'handles nil build times' do - create(:ci_pipeline, duration: nil, project: @pipeline.project) + create(:ci_empty_pipeline, project: project, duration: nil) - chart = Ci::Charts::BuildTime.new(@pipeline.project) - expect(chart.build_times).to eq([2, 0]) + is_expected.to contain_exactly(2, 0) end end end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 61490555ff5a4039954e684d1148e5ccf90a825f..be51d942af7c140d00f5e5459e7fece59469b04c 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -19,7 +19,7 @@ module Ci expect(config_processor.builds_for_stage_and_ref(type, "master").first).to eq({ stage: "test", stage_idx: 1, - name: :rspec, + name: "rspec", commands: "pwd\nrspec", tag_list: [], options: {}, @@ -433,7 +433,7 @@ module Ci expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ stage: "test", stage_idx: 1, - name: :rspec, + name: "rspec", commands: "pwd\nrspec", tag_list: [], options: { @@ -461,7 +461,7 @@ module Ci expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ stage: "test", stage_idx: 1, - name: :rspec, + name: "rspec", commands: "pwd\nrspec", tag_list: [], options: { @@ -533,10 +533,6 @@ module Ci } end - context 'when also global variables are defined' do - - end - context 'when syntax is correct' do let(:variables) do { VAR1: 'value1', VAR2: 'value2' } @@ -704,7 +700,7 @@ module Ci expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ stage: "test", stage_idx: 1, - name: :rspec, + name: "rspec", commands: "pwd\nrspec", tag_list: [], options: { @@ -841,7 +837,7 @@ module Ci expect(subject.first).to eq({ stage: "test", stage_idx: 1, - name: :normal_job, + name: "normal_job", commands: "test", tag_list: [], options: {}, @@ -886,7 +882,7 @@ module Ci expect(subject.first).to eq({ stage: "build", stage_idx: 0, - name: :job1, + name: "job1", commands: "execute-script-for-job", tag_list: [], options: {}, @@ -898,7 +894,7 @@ module Ci expect(subject.second).to eq({ stage: "build", stage_idx: 0, - name: :job2, + name: "job2", commands: "execute-script-for-job", tag_list: [], options: {}, diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb index b12a7b98d4d6bbab0a70e128ceb4d62ec36486b6..36c77206a3f935255113d977ddd8074ad7bc4acc 100644 --- a/spec/lib/extracts_path_spec.rb +++ b/spec/lib/extracts_path_spec.rb @@ -30,15 +30,28 @@ expect(@logs_path).to eq("/#{@project.path_with_namespace}/refs/#{ref}/logs_tree/files/ruby/popen.rb") end - context 'escaped sequences in ref' do - let(:ref) { "improve%2Fawesome" } + context 'escaped slash character in ref' do + let(:ref) { 'improve%2Fawesome' } - it "id has no escape sequences" do + it 'has no escape sequences in @ref or @logs_path' do assign_ref_vars + expect(@ref).to eq('improve/awesome') expect(@logs_path).to eq("/#{@project.path_with_namespace}/refs/#{ref}/logs_tree/files/ruby/popen.rb") end end + + context 'ref contains %20' do + let(:ref) { 'foo%20bar' } + + it 'is not converted to a space in @id' do + @project.repository.add_branch(@project.owner, 'foo%20bar', 'master') + + assign_ref_vars + + expect(@id).to start_with('foo%20bar/') + end + end end describe '#extract_ref' do diff --git a/spec/lib/gitlab/badge/build/metadata_spec.rb b/spec/lib/gitlab/badge/build/metadata_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..ad5388215c27cf5c2d0bcc0e70aa8823ca270265 --- /dev/null +++ b/spec/lib/gitlab/badge/build/metadata_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Gitlab::Badge::Build::Metadata do + let(:project) { create(:project) } + let(:branch) { 'master' } + let(:badge) { described_class.new(project, branch) } + + describe '#to_html' do + let(:html) { Nokogiri::HTML.parse(badge.to_html) } + let(:a_href) { html.at('a') } + + it 'points to link' do + expect(a_href[:href]).to eq badge.link_url + end + + it 'contains clickable image' do + expect(a_href.children.first.name).to eq 'img' + end + end + + describe '#to_markdown' do + subject { badge.to_markdown } + + it { is_expected.to include badge.image_url } + it { is_expected.to include badge.link_url } + end + + describe '#image_url' do + subject { badge.image_url } + it { is_expected.to include "badges/#{branch}/build.svg" } + end + + describe '#link_url' do + subject { badge.link_url } + it { is_expected.to include "commits/#{branch}" } + end +end diff --git a/spec/lib/gitlab/badge/build/template_spec.rb b/spec/lib/gitlab/badge/build/template_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..86dead3c54e1c2089fc7f6bd186a81d35be99bc4 --- /dev/null +++ b/spec/lib/gitlab/badge/build/template_spec.rb @@ -0,0 +1,76 @@ +require 'spec_helper' + +describe Gitlab::Badge::Build::Template do + let(:status) { 'success' } + let(:template) { described_class.new(status) } + + describe '#key_text' do + it 'is always says build' do + expect(template.key_text).to eq 'build' + end + end + + describe '#value_text' do + it 'is status value' do + expect(template.value_text).to eq 'success' + end + end + + describe 'widths and text anchors' do + it 'has fixed width and text anchors' do + expect(template.width).to eq 92 + expect(template.key_width).to eq 38 + expect(template.value_width).to eq 54 + expect(template.key_text_anchor).to eq 19 + expect(template.value_text_anchor).to eq 65 + end + end + + describe '#key_color' do + it 'is always the same' do + expect(template.key_color).to eq '#555' + end + end + + describe '#value_color' do + context 'when status is success' do + let(:status) { 'success' } + + it 'has expected color' do + expect(template.value_color).to eq '#4c1' + end + end + + context 'when status is failed' do + let(:status) { 'failed' } + + it 'has expected color' do + expect(template.value_color).to eq '#e05d44' + end + end + + context 'when status is running' do + let(:status) { 'running' } + + it 'has expected color' do + expect(template.value_color).to eq '#dfb317' + end + end + + context 'when status is unknown' do + let(:status) { 'unknown' } + + it 'has expected color' do + expect(template.value_color).to eq '#9f9f9f' + end + end + + context 'when status does not match any known statuses' do + let(:status) { 'invalid status' } + + it 'has expected color' do + expect(template.value_color).to eq '#9f9f9f' + end + end + end +end diff --git a/spec/lib/gitlab/badge/build_spec.rb b/spec/lib/gitlab/badge/build_spec.rb index f3b522a02f52d173e5fa06f0a8e09775c5dca69c..bb8144d5122c86c7e2a13662225d96c8c5989a42 100644 --- a/spec/lib/gitlab/badge/build_spec.rb +++ b/spec/lib/gitlab/badge/build_spec.rb @@ -6,39 +6,17 @@ let(:branch) { 'master' } let(:badge) { described_class.new(project, branch) } - describe '#type' do - subject { badge.type } - it { is_expected.to eq 'image/svg+xml' } - end - - describe '#to_html' do - let(:html) { Nokogiri::HTML.parse(badge.to_html) } - let(:a_href) { html.at('a') } - - it 'points to link' do - expect(a_href[:href]).to eq badge.link_url - end - - it 'contains clickable image' do - expect(a_href.children.first.name).to eq 'img' + describe '#metadata' do + it 'returns badge metadata' do + expect(badge.metadata.image_url) + .to include 'badges/master/build.svg' end end - describe '#to_markdown' do - subject { badge.to_markdown } - - it { is_expected.to include badge.image_url } - it { is_expected.to include badge.link_url } - end - - describe '#image_url' do - subject { badge.image_url } - it { is_expected.to include "badges/#{branch}/build.svg" } - end - - describe '#link_url' do - subject { badge.link_url } - it { is_expected.to include "commits/#{branch}" } + describe '#key_text' do + it 'always says build' do + expect(badge.key_text).to eq 'build' + end end context 'build exists' do @@ -47,16 +25,15 @@ context 'build success' do before { build.success! } - describe '#to_s' do - subject { badge.to_s } - it { is_expected.to eq 'build-success' } + describe '#status' do + it 'is successful' do + expect(badge.status).to eq 'success' + end end - describe '#data' do - let(:data) { badge.data } - - it 'contains information about success' do - expect(status_node(data, 'success')).to be_truthy + describe '#value_text' do + it 'returns correct value text' do + expect(badge.value_text).to eq 'success' end end end @@ -64,60 +41,66 @@ context 'build failed' do before { build.drop! } - describe '#to_s' do - subject { badge.to_s } - it { is_expected.to eq 'build-failed' } + describe '#status' do + it 'failed' do + expect(badge.status).to eq 'failed' + end end - describe '#data' do - let(:data) { badge.data } - - it 'contains information about failure' do - expect(status_node(data, 'failed')).to be_truthy + describe '#value_text' do + it 'has correct value text' do + expect(badge.value_text).to eq 'failed' end end end - end - context 'build does not exist' do - describe '#to_s' do - subject { badge.to_s } - it { is_expected.to eq 'build-unknown' } + context 'when outdated pipeline for given ref exists' do + before do + build.success! + + old_build = create_build(project, '11eeffdd', branch) + old_build.drop! + end + + it 'does not take outdated pipeline into account' do + expect(badge.status).to eq 'success' + end end - describe '#data' do - let(:data) { badge.data } + context 'when multiple pipelines exist for given sha' do + before do + build.drop! + + new_build = create_build(project, sha, branch) + new_build.success! + end - it 'contains infromation about unknown build' do - expect(status_node(data, 'unknown')).to be_truthy + it 'reports the compound status' do + expect(badge.status).to eq 'failed' end end end - context 'when outdated pipeline for given ref exists' do - before do - build = create_build(project, sha, branch) - build.success! - - old_build = create_build(project, '11eeffdd', branch) - old_build.drop! + context 'build does not exist' do + describe '#status' do + it 'is unknown' do + expect(badge.status).to eq 'unknown' + end end - it 'does not take outdated pipeline into account' do - expect(badge.to_s).to eq 'build-success' + describe '#value_text' do + it 'has correct value text' do + expect(badge.value_text).to eq 'unknown' + end end end def create_build(project, sha, branch) - pipeline = create(:ci_pipeline, project: project, - sha: sha, - ref: branch) + pipeline = create(:ci_empty_pipeline, + project: project, + sha: sha, + ref: branch) create(:ci_build, pipeline: pipeline, stage: 'notify') end - - def status_node(data, status) - xml = Nokogiri::XML.parse(data) - xml.at(%Q{text:contains("#{status}")}) - end end diff --git a/spec/lib/gitlab/changes_list_spec.rb b/spec/lib/gitlab/changes_list_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..69d86144e321fb4a73eff64f1ab2cf456a32b669 --- /dev/null +++ b/spec/lib/gitlab/changes_list_spec.rb @@ -0,0 +1,30 @@ +require "spec_helper" + +describe Gitlab::ChangesList do + let(:valid_changes_string) { "\n000000 570e7b2 refs/heads/my_branch\nd14d6c 6fd24d refs/heads/master" } + let(:invalid_changes) { 1 } + + context 'when changes is a valid string' do + let(:changes_list) { Gitlab::ChangesList.new(valid_changes_string) } + + it 'splits elements by newline character' do + expect(changes_list).to contain_exactly({ + oldrev: "000000", + newrev: "570e7b2", + ref: "refs/heads/my_branch" + }, { + oldrev: "d14d6c", + newrev: "6fd24d", + ref: "refs/heads/master" + }) + end + + it 'behaves like a list' do + expect(changes_list.first).to eq({ + oldrev: "000000", + newrev: "570e7b2", + ref: "refs/heads/my_branch" + }) + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/validatable_spec.rb b/spec/lib/gitlab/ci/config/node/validatable_spec.rb index 10cd01afcd1da4e16314af3ab8a0ca7f34a493ff..64b77fd6e0356f00ec11ed2849d4b2c7845ada96 100644 --- a/spec/lib/gitlab/ci/config/node/validatable_spec.rb +++ b/spec/lib/gitlab/ci/config/node/validatable_spec.rb @@ -23,6 +23,10 @@ .to be Gitlab::Ci::Config::Node::Validator end + it 'returns only one validator to mitigate leaks' do + expect { node.validator }.not_to change { node.validator } + end + context 'when validating node instance' do let(:node_instance) { node.new } diff --git a/spec/lib/gitlab/git/hook_spec.rb b/spec/lib/gitlab/git/hook_spec.rb index a15aa173fbd314baf0435024a1ef9e6cfabcc12e..d1f947b68500940e51248da3b45039c40720c329 100644 --- a/spec/lib/gitlab/git/hook_spec.rb +++ b/spec/lib/gitlab/git/hook_spec.rb @@ -25,7 +25,6 @@ def create_failing_hook(name) end ['pre-receive', 'post-receive', 'update'].each do |hook_name| - context "when triggering a #{hook_name} hook" do context "when the hook is successful" do it "returns success with no errors" do diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index e70bb49c3a29af438af3fd2c9b9952bbdaa14e81..830192f9c762708c8ea4dfadbe1d48d97b25b628 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -24,11 +24,11 @@ def disable_protocol(protocol) end it 'blocks ssh git push' do - expect(@acc.check('git-receive-pack').allowed?).to be_falsey + expect(@acc.check('git-receive-pack', '_any').allowed?).to be_falsey end it 'blocks ssh git pull' do - expect(@acc.check('git-upload-pack').allowed?).to be_falsey + expect(@acc.check('git-upload-pack', '_any').allowed?).to be_falsey end end @@ -39,17 +39,17 @@ def disable_protocol(protocol) end it 'blocks http push' do - expect(@acc.check('git-receive-pack').allowed?).to be_falsey + expect(@acc.check('git-receive-pack', '_any').allowed?).to be_falsey end it 'blocks http git pull' do - expect(@acc.check('git-upload-pack').allowed?).to be_falsey + expect(@acc.check('git-upload-pack', '_any').allowed?).to be_falsey end end end describe 'download_access_check' do - subject { access.check('git-upload-pack') } + subject { access.check('git-upload-pack', '_any') } describe 'master permissions' do before { project.team << [user, :master] } @@ -549,7 +549,7 @@ def self.run_permission_checks(permissions_matrix) let(:actor) { key } context 'push code' do - subject { access.check('git-receive-pack') } + subject { access.check('git-receive-pack', '_any') } context 'when project is authorized' do before { key.projects << project } diff --git a/spec/lib/gitlab/github_import/branch_formatter_spec.rb b/spec/lib/gitlab/github_import/branch_formatter_spec.rb index fc9d5204148f31259ec76c386d42b9d758b917f3..e5300dbba1ee4951100965e66334d277dea00a35 100644 --- a/spec/lib/gitlab/github_import/branch_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/branch_formatter_spec.rb @@ -32,20 +32,6 @@ end end - describe '#name' do - it 'returns raw ref when branch exists' do - branch = described_class.new(project, double(raw)) - - expect(branch.name).to eq 'feature' - end - - it 'returns formatted ref when branch does not exist' do - branch = described_class.new(project, double(raw.merge(ref: 'removed-branch', sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b'))) - - expect(branch.name).to eq 'removed-branch-2e5d3239' - end - end - describe '#repo' do it 'returns raw repo' do branch = described_class.new(project, double(raw)) diff --git a/spec/lib/gitlab/github_import/hook_formatter_spec.rb b/spec/lib/gitlab/github_import/hook_formatter_spec.rb deleted file mode 100644 index 110ba428258eceb74d2c2ebbdc6cc9747a2444ef..0000000000000000000000000000000000000000 --- a/spec/lib/gitlab/github_import/hook_formatter_spec.rb +++ /dev/null @@ -1,65 +0,0 @@ -require 'spec_helper' - -describe Gitlab::GithubImport::HookFormatter, lib: true do - describe '#id' do - it 'returns raw id' do - raw = double(id: 100000) - formatter = described_class.new(raw) - expect(formatter.id).to eq 100000 - end - end - - describe '#name' do - it 'returns raw id' do - raw = double(name: 'web') - formatter = described_class.new(raw) - expect(formatter.name).to eq 'web' - end - end - - describe '#config' do - it 'returns raw config.attrs' do - raw = double(config: double(attrs: { url: 'http://something.com/webhook' })) - formatter = described_class.new(raw) - expect(formatter.config).to eq({ url: 'http://something.com/webhook' }) - end - end - - describe '#valid?' do - it 'returns true when events contains the wildcard event' do - raw = double(events: ['*', 'commit_comment'], active: true) - formatter = described_class.new(raw) - expect(formatter.valid?).to eq true - end - - it 'returns true when events contains the create event' do - raw = double(events: ['create', 'commit_comment'], active: true) - formatter = described_class.new(raw) - expect(formatter.valid?).to eq true - end - - it 'returns true when events contains delete event' do - raw = double(events: ['delete', 'commit_comment'], active: true) - formatter = described_class.new(raw) - expect(formatter.valid?).to eq true - end - - it 'returns true when events contains pull_request event' do - raw = double(events: ['pull_request', 'commit_comment'], active: true) - formatter = described_class.new(raw) - expect(formatter.valid?).to eq true - end - - it 'returns false when events does not contains branch related events' do - raw = double(events: ['member', 'commit_comment'], active: true) - formatter = described_class.new(raw) - expect(formatter.valid?).to eq false - end - - it 'returns false when hook is not active' do - raw = double(events: ['pull_request', 'commit_comment'], active: false) - formatter = described_class.new(raw) - expect(formatter.valid?).to eq false - end - end -end diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index 79931ecd134e9eb40da229119a01a3aaa4738daf..aa28e360993434ba264a3362a65a4d5f5ac07e75 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -9,6 +9,7 @@ let(:source_branch) { double(ref: 'feature', repo: source_repo, sha: source_sha) } let(:target_repo) { repository } let(:target_branch) { double(ref: 'master', repo: target_repo, sha: target_sha) } + let(:removed_branch) { double(ref: 'removed-branch', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') } let(:octocat) { double(id: 123456, login: 'octocat') } let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } @@ -165,6 +166,42 @@ end end + describe '#source_branch_name' do + context 'when source branch exists' do + let(:raw_data) { double(base_data) } + + it 'returns branch ref' do + expect(pull_request.source_branch_name).to eq 'feature' + end + end + + context 'when source branch does not exist' do + let(:raw_data) { double(base_data.merge(head: removed_branch)) } + + it 'prefixes branch name with pull request number' do + expect(pull_request.source_branch_name).to eq 'pull/1347/removed-branch' + end + end + end + + describe '#target_branch_name' do + context 'when source branch exists' do + let(:raw_data) { double(base_data) } + + it 'returns branch ref' do + expect(pull_request.target_branch_name).to eq 'master' + end + end + + context 'when target branch does not exist' do + let(:raw_data) { double(base_data.merge(base: removed_branch)) } + + it 'prefixes branch name with pull request number' do + expect(pull_request.target_branch_name).to eq 'pull/1347/removed-branch' + end + end + end + describe '#valid?' do context 'when source, and target repos are not a fork' do let(:raw_data) { double(base_data) } @@ -178,8 +215,8 @@ let(:source_repo) { double(id: 2) } let(:raw_data) { double(base_data) } - it 'returns false' do - expect(pull_request.valid?).to eq false + it 'returns true' do + expect(pull_request.valid?).to eq true end end @@ -187,8 +224,8 @@ let(:target_repo) { double(id: 2) } let(:raw_data) { double(base_data) } - it 'returns false' do - expect(pull_request.valid?).to eq false + it 'returns true' do + expect(pull_request.valid?).to eq true end end end diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index b5550ca196322a88e4271e13e24941e68fdb7b51..cbbf98dca94f36490dc0e91d5b2c7cb32f17e859 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -2393,7 +2393,7 @@ "source_project_id": 5, "author_id": 1, "assignee_id": null, - "title": "Cannot be automatically merged", + "title": "MR1", "created_at": "2016-06-14T15:02:36.568Z", "updated_at": "2016-06-14T15:02:56.815Z", "state": "opened", @@ -2827,10 +2827,10 @@ "id": 26, "target_branch": "master", "source_branch": "feature", - "source_project_id": 5, + "source_project_id": 4, "author_id": 1, "assignee_id": null, - "title": "Can be automatically merged", + "title": "MR2", "created_at": "2016-06-14T15:02:36.418Z", "updated_at": "2016-06-14T15:02:57.013Z", "state": "opened", diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 32c0d6462f14917758dbb71bfb356de5ccac0756..4d857945fdef667650806af7b437f344637b02c1 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -2,7 +2,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do describe 'restore project tree' do - let(:user) { create(:user) } let(:namespace) { create(:namespace, owner: user) } let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') } @@ -72,6 +71,28 @@ expect(Milestone.find_by_description('test milestone').issues).not_to be_empty end + + context 'Merge requests' do + before do + restored_project_json + end + + it 'always has the new project as a target' do + expect(MergeRequest.find_by_title('MR1').target_project).to eq(project) + end + + it 'has the same source project as originally if source/target are the same' do + expect(MergeRequest.find_by_title('MR1').source_project).to eq(project) + end + + it 'has the new project as target if source/target differ' do + expect(MergeRequest.find_by_title('MR2').target_project).to eq(project) + end + + it 'has no source if source/target differ' do + expect(MergeRequest.find_by_title('MR2').source_project_id).to eq(-1) + end + end end end end diff --git a/spec/lib/gitlab/redis_spec.rb b/spec/lib/gitlab/redis_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..e54f5ffb3124930a2d2aa9e4e4715ece5676dffc --- /dev/null +++ b/spec/lib/gitlab/redis_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +describe Gitlab::Redis do + let(:redis_config) { Rails.root.join('config', 'resque.yml').to_s } + + before(:each) { described_class.reset_params! } + after(:each) { described_class.reset_params! } + + describe '.params' do + subject { described_class.params } + + context 'when url contains unix socket reference' do + let(:config_old) { Rails.root.join('spec/fixtures/config/redis_old_format_socket.yml').to_s } + let(:config_new) { Rails.root.join('spec/fixtures/config/redis_new_format_socket.yml').to_s } + + context 'with old format' do + it 'returns path key instead' do + expect_any_instance_of(described_class).to receive(:config_file) { config_old } + + is_expected.to include(path: '/path/to/old/redis.sock') + is_expected.not_to have_key(:url) + end + end + + context 'with new format' do + it 'returns path key instead' do + expect_any_instance_of(described_class).to receive(:config_file) { config_new } + + is_expected.to include(path: '/path/to/redis.sock') + is_expected.not_to have_key(:url) + end + end + end + + context 'when url is host based' do + let(:config_old) { Rails.root.join('spec/fixtures/config/redis_old_format_host.yml') } + let(:config_new) { Rails.root.join('spec/fixtures/config/redis_new_format_host.yml') } + + context 'with old format' do + it 'returns hash with host, port, db, and password' do + expect_any_instance_of(described_class).to receive(:config_file) { config_old } + + is_expected.to include(host: 'localhost', password: 'mypassword', port: 6379, db: 99) + is_expected.not_to have_key(:url) + end + end + + context 'with new format' do + it 'returns hash with host, port, db, and password' do + expect_any_instance_of(described_class).to receive(:config_file) { config_new } + + is_expected.to include(host: 'localhost', password: 'mynewpassword', port: 6379, db: 99) + is_expected.not_to have_key(:url) + end + end + end + end + + describe '#raw_config_hash' do + it 'returns default redis url when no config file is present' do + expect(subject).to receive(:fetch_config) { false } + + expect(subject.send(:raw_config_hash)).to eq(url: Gitlab::Redis::DEFAULT_REDIS_URL) + end + + it 'returns old-style single url config in a hash' do + expect(subject).to receive(:fetch_config) { 'redis://myredis:6379' } + expect(subject.send(:raw_config_hash)).to eq(url: 'redis://myredis:6379') + end + end + + describe '#fetch_config' do + it 'returns false when no config file is present' do + allow(File).to receive(:exist?).with(redis_config) { false } + + expect(subject.send(:fetch_config)).to be_falsey + end + end +end diff --git a/spec/lib/gitlab/upgrader_spec.rb b/spec/lib/gitlab/upgrader_spec.rb index 5be78680a9a89cf1c91d5fe3b6e37a5b9b558a96..109b7a7c93d95abc074e2800030c85df6e112434 100644 --- a/spec/lib/gitlab/upgrader_spec.rb +++ b/spec/lib/gitlab/upgrader_spec.rb @@ -16,6 +16,11 @@ end describe 'latest_version_raw' do + it 'is the latest version for GitLab 5' do + allow(upgrader).to receive(:current_version_raw).and_return("5.3.0") + expect(upgrader.latest_version_raw).to eq("v5.4.2") + end + it 'gets the latest version from tags' do allow(upgrader).to receive(:fetch_git_tags).and_return([ '6f0733310546402c15d3ae6128a95052f6c8ea96 refs/tags/v7.1.1-ee', diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 9ecc9aac84bb7fe9bf4a8474f0d6fa4ec53f6991..5980f6ddc32b1a21e9c399af4808e11360c490ba 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -764,6 +764,53 @@ def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now) end end + describe '#when' do + subject { build.when } + + context 'if is undefined' do + before do + build.when = nil + end + + context 'use from gitlab-ci.yml' do + before do + stub_ci_pipeline_yaml_file(config) + end + + context 'if config is not found' do + let(:config) { nil } + + it { is_expected.to eq('on_success') } + end + + context 'if config does not have a questioned job' do + let(:config) do + YAML.dump({ + test_other: { + script: 'Hello World' + } + }) + end + + it { is_expected.to eq('on_success') } + end + + context 'if config has when' do + let(:config) do + YAML.dump({ + test: { + script: 'Hello World', + when: 'always' + } + }) + end + + it { is_expected.to eq('always') } + end + end + end + end + describe '#retryable?' do context 'when build is running' do before do @@ -839,8 +886,10 @@ def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now) is_expected.to eq(build) end - context 'for success build' do - before { build.queue } + context 'for successful build' do + before do + build.update(status: 'success') + end it 'creates a new build' do is_expected.to be_pending diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index ccee591cf7a50e0624cd69b8aaa391918572a908..950833cb219a2476b819420559cc5fdb5b9b2146 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -2,7 +2,7 @@ describe Ci::Pipeline, models: true do let(:project) { FactoryGirl.create :empty_project } - let(:pipeline) { FactoryGirl.create :ci_pipeline, project: project } + let(:pipeline) { FactoryGirl.create :ci_empty_pipeline, project: project } it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:user) } @@ -38,9 +38,6 @@ it { expect(pipeline.sha).to start_with(subject) } end - describe '#create_next_builds' do - end - describe '#retried' do subject { pipeline.retried } @@ -54,312 +51,9 @@ end end - describe '#create_builds' do - let!(:pipeline) { FactoryGirl.create :ci_pipeline, project: project, ref: 'master', tag: false } - - def create_builds(trigger_request = nil) - pipeline.create_builds(nil, trigger_request) - end - - def create_next_builds - pipeline.create_next_builds(pipeline.builds.order(:id).last) - end - - it 'creates builds' do - expect(create_builds).to be_truthy - pipeline.builds.update_all(status: "success") - expect(pipeline.builds.count(:all)).to eq(2) - - expect(create_next_builds).to be_truthy - pipeline.builds.update_all(status: "success") - expect(pipeline.builds.count(:all)).to eq(4) - - expect(create_next_builds).to be_truthy - pipeline.builds.update_all(status: "success") - expect(pipeline.builds.count(:all)).to eq(5) - - expect(create_next_builds).to be_falsey - end - - context 'custom stage with first job allowed to fail' do - let(:yaml) do - { - stages: ['clean', 'test'], - clean_job: { - stage: 'clean', - allow_failure: true, - script: 'BUILD', - }, - test_job: { - stage: 'test', - script: 'TEST', - }, - } - end - - before do - stub_ci_pipeline_yaml_file(YAML.dump(yaml)) - create_builds - end - - it 'properly schedules builds' do - expect(pipeline.builds.pluck(:status)).to contain_exactly('pending') - pipeline.builds.running_or_pending.each(&:drop) - expect(pipeline.builds.pluck(:status)).to contain_exactly('pending', 'failed') - end - end - - context 'properly creates builds when "when" is defined' do - let(:yaml) do - { - stages: ["build", "test", "test_failure", "deploy", "cleanup"], - build: { - stage: "build", - script: "BUILD", - }, - test: { - stage: "test", - script: "TEST", - }, - test_failure: { - stage: "test_failure", - script: "ON test failure", - when: "on_failure", - }, - deploy: { - stage: "deploy", - script: "PUBLISH", - }, - cleanup: { - stage: "cleanup", - script: "TIDY UP", - when: "always", - } - } - end - - before do - stub_ci_pipeline_yaml_file(YAML.dump(yaml)) - end - - context 'when builds are successful' do - it 'properly creates builds' do - expect(create_builds).to be_truthy - expect(pipeline.builds.pluck(:name)).to contain_exactly('build') - expect(pipeline.builds.pluck(:status)).to contain_exactly('pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test') - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy') - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup') - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success') - pipeline.reload - expect(pipeline.status).to eq('success') - end - end - - context 'when test job fails' do - it 'properly creates builds' do - expect(create_builds).to be_truthy - expect(pipeline.builds.pluck(:name)).to contain_exactly('build') - expect(pipeline.builds.pluck(:status)).to contain_exactly('pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test') - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending') - pipeline.builds.running_or_pending.each(&:drop) - - expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure') - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success') - pipeline.reload - expect(pipeline.status).to eq('failed') - end - end - - context 'when test and test_failure jobs fail' do - it 'properly creates builds' do - expect(create_builds).to be_truthy - expect(pipeline.builds.pluck(:name)).to contain_exactly('build') - expect(pipeline.builds.pluck(:status)).to contain_exactly('pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test') - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending') - pipeline.builds.running_or_pending.each(&:drop) - - expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure') - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending') - pipeline.builds.running_or_pending.each(&:drop) - - expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success') - pipeline.reload - expect(pipeline.status).to eq('failed') - end - end - - context 'when deploy job fails' do - it 'properly creates builds' do - expect(create_builds).to be_truthy - expect(pipeline.builds.pluck(:name)).to contain_exactly('build') - expect(pipeline.builds.pluck(:status)).to contain_exactly('pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test') - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy') - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending') - pipeline.builds.running_or_pending.each(&:drop) - - expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup') - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success') - pipeline.reload - expect(pipeline.status).to eq('failed') - end - end - - context 'when build is canceled in the second stage' do - it 'does not schedule builds after build has been canceled' do - expect(create_builds).to be_truthy - expect(pipeline.builds.pluck(:name)).to contain_exactly('build') - expect(pipeline.builds.pluck(:status)).to contain_exactly('pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(pipeline.builds.running_or_pending).not_to be_empty - - expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test') - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending') - pipeline.builds.running_or_pending.each(&:cancel) - - expect(pipeline.builds.running_or_pending).to be_empty - expect(pipeline.reload.status).to eq('canceled') - end - end - - context 'when listing manual actions' do - let(:yaml) do - { - stages: ["build", "test", "staging", "production", "cleanup"], - build: { - stage: "build", - script: "BUILD", - }, - test: { - stage: "test", - script: "TEST", - }, - staging: { - stage: "staging", - script: "PUBLISH", - }, - production: { - stage: "production", - script: "PUBLISH", - when: "manual", - }, - cleanup: { - stage: "cleanup", - script: "TIDY UP", - when: "always", - }, - clear_cache: { - stage: "cleanup", - script: "CLEAR CACHE", - when: "manual", - } - } - end - - it 'returns only for skipped builds' do - # currently all builds are created - expect(create_builds).to be_truthy - expect(manual_actions).to be_empty - - # succeed stage build - pipeline.builds.running_or_pending.each(&:success) - expect(manual_actions).to be_empty - - # succeed stage test - pipeline.builds.running_or_pending.each(&:success) - expect(manual_actions).to be_empty - - # succeed stage staging and skip stage production - pipeline.builds.running_or_pending.each(&:success) - expect(manual_actions).to be_many # production and clear cache - - # succeed stage cleanup - pipeline.builds.running_or_pending.each(&:success) - - # after processing a pipeline we should have 6 builds, 5 succeeded - expect(pipeline.builds.count).to eq(6) - expect(pipeline.builds.success.count).to eq(4) - end - - def manual_actions - pipeline.manual_actions - end - end - end - - context 'when no builds created' do - let(:pipeline) { build(:ci_pipeline) } - - before do - stub_ci_pipeline_yaml_file(YAML.dump(before_script: ['ls'])) - end - - it 'returns false' do - expect(pipeline.create_builds(nil)).to be_falsey - expect(pipeline).not_to be_persisted - end - end - end - - describe "#finished_at" do - let(:pipeline) { FactoryGirl.create :ci_pipeline } - - it "returns finished_at of latest build" do - build = FactoryGirl.create :ci_build, pipeline: pipeline, finished_at: Time.now - 60 - FactoryGirl.create :ci_build, pipeline: pipeline, finished_at: Time.now - 120 - - expect(pipeline.finished_at.to_i).to eq(build.finished_at.to_i) - end - - it "returns nil if there is no finished build" do - FactoryGirl.create :ci_not_started_build, pipeline: pipeline - - expect(pipeline.finished_at).to be_nil - end - end - describe "coverage" do let(:project) { FactoryGirl.create :empty_project, build_coverage_regex: "/.*/" } - let(:pipeline) { FactoryGirl.create :ci_pipeline, project: project } + let(:pipeline) { FactoryGirl.create :ci_empty_pipeline, project: project } it "calculates average when there are two builds with coverage" do FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline @@ -426,33 +120,47 @@ def manual_actions end end - describe '#update_state' do - it 'executes update_state after touching object' do - expect(pipeline).to receive(:update_state).and_return(true) - pipeline.touch + describe 'state machine' do + let(:current) { Time.now.change(usec: 0) } + let(:build) { create :ci_build, name: 'build1', pipeline: pipeline, started_at: current - 60, finished_at: current } + let(:build2) { create :ci_build, name: 'build2', pipeline: pipeline, started_at: current - 60, finished_at: current } + + describe '#duration' do + before do + build.skip + build2.skip + end + + it 'matches sum of builds duration' do + expect(pipeline.reload.duration).to eq(build.duration + build2.duration) + end end - context 'dependent objects' do - let(:commit_status) { build :commit_status, pipeline: pipeline } + describe '#started_at' do + it 'updates on transitioning to running' do + build.run - it 'executes update_state after saving dependent object' do - expect(pipeline).to receive(:update_state).and_return(true) - commit_status.save + expect(pipeline.reload.started_at).not_to be_nil + end + + it 'does not update on transitioning to success' do + build.success + + expect(pipeline.reload.started_at).to be_nil end end - context 'update state' do - let(:current) { Time.now.change(usec: 0) } - let(:build) { FactoryGirl.create :ci_build, :success, pipeline: pipeline, started_at: current - 120, finished_at: current - 60 } + describe '#finished_at' do + it 'updates on transitioning to success' do + build.success - before do - build + expect(pipeline.reload.finished_at).not_to be_nil end - [:status, :started_at, :finished_at, :duration].each do |param| - it "update #{param}" do - expect(pipeline.send(param)).to eq(build.send(param)) - end + it 'does not update on transitioning to running' do + build.run + + expect(pipeline.reload.finished_at).to be_nil end end end @@ -542,4 +250,64 @@ def manual_actions end end end + + describe '#status' do + let!(:build) { create(:ci_build, :created, pipeline: pipeline, name: 'test') } + + subject { pipeline.reload.status } + + context 'on queuing' do + before do + build.enqueue + end + + it { is_expected.to eq('pending') } + end + + context 'on run' do + before do + build.enqueue + build.run + end + + it { is_expected.to eq('running') } + end + + context 'on drop' do + before do + build.drop + end + + it { is_expected.to eq('failed') } + end + + context 'on success' do + before do + build.success + end + + it { is_expected.to eq('success') } + end + + context 'on cancel' do + before do + build.cancel + end + + it { is_expected.to eq('canceled') } + end + + context 'on failure and build retry' do + before do + build.drop + Ci::Build.retry(build) + end + + # We are changing a state: created > failed > running + # Instead of: created > failed > pending + # Since the pipeline already run, so it should not be pending anymore + + it { is_expected.to eq('running') } + end + end end diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb index 4078b9e4ff54a57f07ce000b4127cbc07edac238..cbdf7eec082d4832c99691babb7cd96396f377eb 100644 --- a/spec/models/hooks/system_hook_spec.rb +++ b/spec/models/hooks/system_hook_spec.rb @@ -38,7 +38,7 @@ end it "project_destroy hook" do - Projects::DestroyService.new(project, user, {}).pending_delete! + Projects::DestroyService.new(project, user, {}).async_execute expect(WebMock).to have_requested(:post, system_hook.url).with( body: /project_destroy/, diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 44cd3c08718fd577d4d4ba0bb9589be42f912743..2277f4e13bfd84877275758fd325e3bebce6869d 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -10,7 +10,7 @@ it { is_expected.to validate_presence_of(:user) } it { is_expected.to validate_presence_of(:source) } - it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) } + it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.all_values) } it_behaves_like 'an object with email-formated attributes', :invite_email do subject { build(:project_member) } diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb index 28673de31895206afaaf95fbf42337708bc292d3..913d74645a7fd8ac04eb78a2358c0fa61e24c073 100644 --- a/spec/models/members/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -27,6 +27,7 @@ describe 'validations' do it { is_expected.to allow_value('Project').for(:source_type) } it { is_expected.not_to allow_value('project').for(:source_type) } + it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) } end describe 'modules' do @@ -40,7 +41,7 @@ end describe "#destroy" do - let(:owner) { create(:project_member, access_level: ProjectMember::OWNER) } + let(:owner) { create(:project_member, access_level: ProjectMember::MASTER) } let(:project) { owner.project } let(:master) { create(:project_member, project: project) } diff --git a/spec/models/project_services/campfire_service_spec.rb b/spec/models/project_services/campfire_service_spec.rb index 3e6da42803b800822d640ae8c7f163fb1a751d5b..1adf93258f3512d5bff2aae38708ec156673ad89 100644 --- a/spec/models/project_services/campfire_service_spec.rb +++ b/spec/models/project_services/campfire_service_spec.rb @@ -39,4 +39,62 @@ it { is_expected.not_to validate_presence_of(:token) } end end + + describe "#execute" do + let(:user) { create(:user) } + let(:project) { create(:project) } + + before do + @campfire_service = CampfireService.new + allow(@campfire_service).to receive_messages( + project_id: project.id, + project: project, + service_hook: true, + token: 'verySecret', + subdomain: 'project-name', + room: 'test-room' + ) + @sample_data = Gitlab::PushDataBuilder.build_sample(project, user) + @rooms_url = 'https://verySecret:X@project-name.campfirenow.com/rooms.json' + @headers = { 'Content-Type' => 'application/json; charset=utf-8' } + end + + it "calls Campfire API to get a list of rooms and speak in a room" do + # make sure a valid list of rooms is returned + body = File.read(Rails.root + 'spec/fixtures/project_services/campfire/rooms.json') + WebMock.stub_request(:get, @rooms_url).to_return( + body: body, + status: 200, + headers: @headers + ) + # stub the speak request with the room id found in the previous request's response + speak_url = 'https://verySecret:X@project-name.campfirenow.com/room/123/speak.json' + WebMock.stub_request(:post, speak_url) + + @campfire_service.execute(@sample_data) + + expect(WebMock).to have_requested(:get, @rooms_url).once + expect(WebMock).to have_requested(:post, speak_url).with( + body: /#{project.path}.*#{@sample_data[:before]}.*#{@sample_data[:after]}/ + ).once + end + + it "calls Campfire API to get a list of rooms but shouldn't speak in a room" do + # return a list of rooms that do not contain a room named 'test-room' + body = File.read(Rails.root + 'spec/fixtures/project_services/campfire/rooms2.json') + WebMock.stub_request(:get, @rooms_url).to_return( + body: body, + status: 200, + headers: @headers + ) + # we want to make sure no request is sent to the /speak endpoint, here is a basic + # regexp that matches this endpoint + speak_url = 'https://verySecret:X@project-name.campfirenow.com/room/.*/speak.json' + + @campfire_service.execute(@sample_data) + + expect(WebMock).to have_requested(:get, @rooms_url).once + expect(WebMock).not_to have_requested(:post, /#{speak_url}/) + end + end end diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index 4e4abcc581457df0a6401a9df805afebb4fac7fa..ff7a1702d8fd0353775bebd3640a6c7b363cd13c 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -161,16 +161,16 @@ expect(WebMock).to have_requested(:post, api_url).once end - context 'merge request message' do - it 'creates a message for opened merge requests' do - message = hipchat.send(:create_merge_request_message, merge_sample_data) + it "creates a merge request message" do + message = hipchat.send(:create_merge_request_message, + merge_sample_data) - obj_attr = merge_sample_data[:object_attributes] - expect(message).to eq("#{user.name} opened " \ - "merge request !#{obj_attr['iid']} in " \ - "#{project_name}: " \ - 'Awesome merge request' \ - '
please fix
') + obj_attr = merge_sample_data[:object_attributes] + expect(message).to eq("#{user.name} opened " \ + "merge request !#{obj_attr['iid']} in " \ + "#{project_name}: " \ + 'Awesome merge request' \ + '
please fix
') end it 'creates a message for approved merge requests' do @@ -304,7 +304,8 @@ end context 'build events' do - let(:build) { create(:ci_build) } + let(:pipeline) { create(:ci_empty_pipeline) } + let(:build) { create(:ci_build, pipeline: pipeline) } let(:data) { Gitlab::BuildDataBuilder.build(build) } context 'for failed' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 5d44c9e31036263fd3c88357e69263f8df2990c3..b49be29431fd3f5e68c408517be80bdb7dd566ce 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -754,6 +754,20 @@ it { expect(project.builds_enabled?).to be_truthy } end + describe '.cached_count', caching: true do + let(:group) { create(:group, :public) } + let!(:project1) { create(:empty_project, :public, group: group) } + let!(:project2) { create(:empty_project, :public, group: group) } + + it 'returns total project count' do + expect(Project).to receive(:count).once.and_call_original + + 3.times do + expect(Project.cached_count).to eq(2) + end + end + end + describe '.trending' do let(:group) { create(:group, :public) } let(:project1) { create(:empty_project, :public, group: group) } diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..d78494b76fac2a281852498c867b575a40a2454e --- /dev/null +++ b/spec/requests/api/access_requests_spec.rb @@ -0,0 +1,246 @@ +require 'spec_helper' + +describe API::AccessRequests, api: true do + include ApiHelpers + + let(:master) { create(:user) } + let(:developer) { create(:user) } + let(:access_requester) { create(:user) } + let(:stranger) { create(:user) } + + let(:project) do + project = create(:project, :public, creator_id: master.id, namespace: master.namespace) + project.team << [developer, :developer] + project.team << [master, :master] + project.request_access(access_requester) + project + end + + let(:group) do + group = create(:group, :public) + group.add_developer(developer) + group.add_owner(master) + group.request_access(access_requester) + group + end + + shared_examples 'GET /:sources/:id/access_requests' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { get api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger) } + end + + context 'when authenticated as a non-master/owner' do + %i[developer access_requester stranger].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + get api("/#{source_type.pluralize}/#{source.id}/access_requests", user) + + expect(response).to have_http_status(403) + end + end + end + end + + context 'when authenticated as a master/owner' do + it 'returns access requesters' do + get api("/#{source_type.pluralize}/#{source.id}/access_requests", master) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + end + end + end + end + + shared_examples 'POST /:sources/:id/access_requests' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { post api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger) } + end + + context 'when authenticated as a member' do + %i[developer master].each do |type| + context "as a #{type}" do + it 'returns 400' do + expect do + user = public_send(type) + post api("/#{source_type.pluralize}/#{source.id}/access_requests", user) + + expect(response).to have_http_status(400) + end.not_to change { source.requesters.count } + end + end + end + end + + context 'when authenticated as an access requester' do + it 'returns 400' do + expect do + post api("/#{source_type.pluralize}/#{source.id}/access_requests", access_requester) + + expect(response).to have_http_status(400) + end.not_to change { source.requesters.count } + end + end + + context 'when authenticated as a stranger' do + it 'returns 201' do + expect do + post api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger) + + expect(response).to have_http_status(201) + end.to change { source.requesters.count }.by(1) + + # User attributes + expect(json_response['id']).to eq(stranger.id) + expect(json_response['name']).to eq(stranger.name) + expect(json_response['username']).to eq(stranger.username) + expect(json_response['state']).to eq(stranger.state) + expect(json_response['avatar_url']).to eq(stranger.avatar_url) + expect(json_response['web_url']).to eq(Gitlab::Routing.url_helpers.user_url(stranger)) + + # Member attributes + expect(json_response['requested_at']).to be_present + end + end + end + end + + shared_examples 'PUT /:sources/:id/access_requests/:user_id/approve' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}/approve", stranger) } + end + + context 'when authenticated as a non-master/owner' do + %i[developer access_requester stranger].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}/approve", user) + + expect(response).to have_http_status(403) + end + end + end + end + + context 'when authenticated as a master/owner' do + it 'returns 201' do + expect do + put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}/approve", master), + access_level: Member::MASTER + + expect(response).to have_http_status(201) + end.to change { source.members.count }.by(1) + # User attributes + expect(json_response['id']).to eq(access_requester.id) + expect(json_response['name']).to eq(access_requester.name) + expect(json_response['username']).to eq(access_requester.username) + expect(json_response['state']).to eq(access_requester.state) + expect(json_response['avatar_url']).to eq(access_requester.avatar_url) + expect(json_response['web_url']).to eq(Gitlab::Routing.url_helpers.user_url(access_requester)) + + # Member attributes + expect(json_response['access_level']).to eq(Member::MASTER) + end + + context 'user_id does not match an existing access requester' do + it 'returns 404' do + expect do + put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{stranger.id}/approve", master) + + expect(response).to have_http_status(404) + end.not_to change { source.members.count } + end + end + end + end + end + + shared_examples 'DELETE /:sources/:id/access_requests/:user_id' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", stranger) } + end + + context 'when authenticated as a non-master/owner' do + %i[developer stranger].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", user) + + expect(response).to have_http_status(403) + end + end + end + end + + context 'when authenticated as the access requester' do + it 'returns 200' do + expect do + delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", access_requester) + + expect(response).to have_http_status(200) + end.to change { source.requesters.count }.by(-1) + end + end + + context 'when authenticated as a master/owner' do + it 'returns 200' do + expect do + delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", master) + + expect(response).to have_http_status(200) + end.to change { source.requesters.count }.by(-1) + end + + context 'user_id does not match an existing access requester' do + it 'returns 404' do + expect do + delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{stranger.id}", master) + + expect(response).to have_http_status(404) + end.not_to change { source.requesters.count } + end + end + end + end + end + + it_behaves_like 'GET /:sources/:id/access_requests', 'project' do + let(:source) { project } + end + + it_behaves_like 'GET /:sources/:id/access_requests', 'group' do + let(:source) { group } + end + + it_behaves_like 'POST /:sources/:id/access_requests', 'project' do + let(:source) { project } + end + + it_behaves_like 'POST /:sources/:id/access_requests', 'group' do + let(:source) { group } + end + + it_behaves_like 'PUT /:sources/:id/access_requests/:user_id/approve', 'project' do + let(:source) { project } + end + + it_behaves_like 'PUT /:sources/:id/access_requests/:user_id/approve', 'group' do + let(:source) { group } + end + + it_behaves_like 'DELETE /:sources/:id/access_requests/:user_id', 'project' do + let(:source) { project } + end + + it_behaves_like 'DELETE /:sources/:id/access_requests/:user_id', 'group' do + let(:source) { group } + end +end diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 8f43fb0bab8a4bb6afae277e79de664a996c9553..c1f7cdb6b140f4703f00247501f5b34813ab15a9 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -9,7 +9,7 @@ let!(:developer) { create(:project_member, :developer, user: user, project: project) } let(:reporter) { create(:project_member, :reporter, project: project) } let(:guest) { create(:project_member, :guest, project: project) } - let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) } + let!(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) } let!(:build) { create(:ci_build, pipeline: pipeline) } describe 'GET /projects/:id/builds ' do @@ -174,7 +174,11 @@ describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do let(:api_user) { reporter.user } - let(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } + let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } + + before do + build.success + end def path_for_ref(ref = pipeline.ref, job = build.name) api("/projects/#{project.id}/builds/artifacts/#{ref}/download?job=#{job}", api_user) diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 4379fcb3c1ee1c9d6a548aae7f5def76c800d993..7ca75d776733343d0988c00cb64f5c190c3042af 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -89,16 +89,29 @@ it "returns nil for commit without CI" do get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) + expect(response).to have_http_status(200) expect(json_response['status']).to be_nil end it "returns status for CI" do pipeline = project.ensure_pipeline(project.repository.commit.sha, 'master') + pipeline.update(status: 'success') + get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) + expect(response).to have_http_status(200) expect(json_response['status']).to eq(pipeline.status) end + + it "returns status for CI when pipeline is created" do + project.ensure_pipeline(project.repository.commit.sha, 'master') + + get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['status']).to be_nil + end end context "unauthorized user" do diff --git a/spec/requests/api/deploy_keys.rb b/spec/requests/api/deploy_keys.rb deleted file mode 100644 index dfaa1483e955d733246a7721fc285dc4619e89f2..0000000000000000000000000000000000000000 --- a/spec/requests/api/deploy_keys.rb +++ /dev/null @@ -1,38 +0,0 @@ -require 'spec_helper' - -describe API::API, api: true do - include ApiHelpers - - let(:user) { create(:user) } - let(:project) { create(:project, creator_id: user.id) } - let!(:deploy_keys_project) { create(:deploy_keys_project, project: project) } - let(:admin) { create(:admin) } - - describe 'GET /deploy_keys' do - before { admin } - - context 'when unauthenticated' do - it 'returns authentication error' do - get api('/deploy_keys') - expect(response.status).to eq(401) - end - end - - context 'when authenticated as non-admin user' do - it 'returns a 403 error' do - get api('/deploy_keys', user) - expect(response.status).to eq(403) - end - end - - context 'when authenticated as admin' do - it 'returns all deploy keys' do - get api('/deploy_keys', admin) - expect(response.status).to eq(200) - - expect(json_response).to be_an Array - expect(json_response.first['id']).to eq(deploy_keys_project.deploy_key.id) - end - end - end -end diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..7d8cc45327c73f339fb10eae5686f5f9dec652dd --- /dev/null +++ b/spec/requests/api/deploy_keys_spec.rb @@ -0,0 +1,160 @@ +require 'spec_helper' + +describe API::API, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:admin) { create(:admin) } + let(:project) { create(:project, creator_id: user.id) } + let(:deploy_key) { create(:deploy_key, public: true) } + + let!(:deploy_keys_project) do + create(:deploy_keys_project, project: project, deploy_key: deploy_key) + end + + describe 'GET /deploy_keys' do + context 'when unauthenticated' do + it 'should return authentication error' do + get api('/deploy_keys') + + expect(response.status).to eq(401) + end + end + + context 'when authenticated as non-admin user' do + it 'should return a 403 error' do + get api('/deploy_keys', user) + + expect(response.status).to eq(403) + end + end + + context 'when authenticated as admin' do + it 'should return all deploy keys' do + get api('/deploy_keys', admin) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['id']).to eq(deploy_keys_project.deploy_key.id) + end + end + end + + describe 'GET /projects/:id/deploy_keys' do + before { deploy_key } + + it 'should return array of ssh keys' do + get api("/projects/#{project.id}/deploy_keys", admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['title']).to eq(deploy_key.title) + end + end + + describe 'GET /projects/:id/deploy_keys/:key_id' do + it 'should return a single key' do + get api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq(deploy_key.title) + end + + it 'should return 404 Not Found with invalid ID' do + get api("/projects/#{project.id}/deploy_keys/404", admin) + + expect(response).to have_http_status(404) + end + end + + describe 'POST /projects/:id/deploy_keys' do + it 'should not create an invalid ssh key' do + post api("/projects/#{project.id}/deploy_keys", admin), { title: 'invalid key' } + + expect(response).to have_http_status(400) + expect(json_response['message']['key']).to eq([ + 'can\'t be blank', + 'is too short (minimum is 0 characters)', + 'is invalid' + ]) + end + + it 'should not create a key without title' do + post api("/projects/#{project.id}/deploy_keys", admin), key: 'some key' + + expect(response).to have_http_status(400) + expect(json_response['message']['title']).to eq([ + 'can\'t be blank', + 'is too short (minimum is 0 characters)' + ]) + end + + it 'should create new ssh key' do + key_attrs = attributes_for :another_key + + expect do + post api("/projects/#{project.id}/deploy_keys", admin), key_attrs + end.to change{ project.deploy_keys.count }.by(1) + end + end + + describe 'DELETE /projects/:id/deploy_keys/:key_id' do + before { deploy_key } + + it 'should delete existing key' do + expect do + delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) + end.to change{ project.deploy_keys.count }.by(-1) + end + + it 'should return 404 Not Found with invalid ID' do + delete api("/projects/#{project.id}/deploy_keys/404", admin) + + expect(response).to have_http_status(404) + end + end + + describe 'POST /projects/:id/deploy_keys/:key_id/enable' do + let(:project2) { create(:empty_project) } + + context 'when the user can admin the project' do + it 'enables the key' do + expect do + post api("/projects/#{project2.id}/deploy_keys/#{deploy_key.id}/enable", admin) + end.to change { project2.deploy_keys.count }.from(0).to(1) + + expect(response).to have_http_status(201) + expect(json_response['id']).to eq(deploy_key.id) + end + end + + context 'when authenticated as non-admin user' do + it 'should return a 404 error' do + post api("/projects/#{project2.id}/deploy_keys/#{deploy_key.id}/enable", user) + + expect(response).to have_http_status(404) + end + end + end + + describe 'DELETE /projects/:id/deploy_keys/:key_id/disable' do + context 'when the user can admin the project' do + it 'disables the key' do + expect do + delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}/disable", admin) + end.to change { project.deploy_keys.count }.from(1).to(0) + + expect(response).to have_http_status(200) + expect(json_response['id']).to eq(deploy_key.id) + end + end + + context 'when authenticated as non-admin user' do + it 'should return a 404 error' do + delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}/disable", user) + + expect(response).to have_http_status(404) + end + end + end +end diff --git a/spec/requests/api/group_members_spec.rb b/spec/requests/api/group_members_spec.rb deleted file mode 100644 index 8bd6a8062ae7745778fda5049fbfe0ef311be9cf..0000000000000000000000000000000000000000 --- a/spec/requests/api/group_members_spec.rb +++ /dev/null @@ -1,199 +0,0 @@ -require 'spec_helper' - -describe API::API, api: true do - include ApiHelpers - - let(:owner) { create(:user) } - let(:reporter) { create(:user) } - let(:developer) { create(:user) } - let(:master) { create(:user) } - let(:guest) { create(:user) } - let(:stranger) { create(:user) } - - let!(:group_with_members) do - group = create(:group, :private) - group.add_users([reporter.id], GroupMember::REPORTER) - group.add_users([developer.id], GroupMember::DEVELOPER) - group.add_users([master.id], GroupMember::MASTER) - group.add_users([guest.id], GroupMember::GUEST) - group - end - - let!(:group_no_members) { create(:group) } - - before do - group_with_members.add_owner owner - group_no_members.add_owner owner - end - - describe "GET /groups/:id/members" do - context "when authenticated as user that is part or the group" do - it "each user: returns an array of members groups of group3" do - [owner, master, developer, reporter, guest].each do |user| - get api("/groups/#{group_with_members.id}/members", user) - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to eq(5) - expect(json_response.find { |e| e['id'] == owner.id }['access_level']).to eq(GroupMember::OWNER) - expect(json_response.find { |e| e['id'] == reporter.id }['access_level']).to eq(GroupMember::REPORTER) - expect(json_response.find { |e| e['id'] == developer.id }['access_level']).to eq(GroupMember::DEVELOPER) - expect(json_response.find { |e| e['id'] == master.id }['access_level']).to eq(GroupMember::MASTER) - expect(json_response.find { |e| e['id'] == guest.id }['access_level']).to eq(GroupMember::GUEST) - end - end - - it 'users not part of the group should get access error' do - get api("/groups/#{group_with_members.id}/members", stranger) - - expect(response).to have_http_status(404) - end - end - end - - describe "POST /groups/:id/members" do - context "when not a member of the group" do - it "does not add guest as member of group_no_members when adding being done by person outside the group" do - post api("/groups/#{group_no_members.id}/members", reporter), user_id: guest.id, access_level: GroupMember::MASTER - expect(response).to have_http_status(403) - end - end - - context "when a member of the group" do - it "returns ok and add new member" do - new_user = create(:user) - - expect do - post api("/groups/#{group_no_members.id}/members", owner), user_id: new_user.id, access_level: GroupMember::MASTER - end.to change { group_no_members.members.count }.by(1) - - expect(response).to have_http_status(201) - expect(json_response['name']).to eq(new_user.name) - expect(json_response['access_level']).to eq(GroupMember::MASTER) - end - - it "does not allow guest to modify group members" do - new_user = create(:user) - - expect do - post api("/groups/#{group_with_members.id}/members", guest), user_id: new_user.id, access_level: GroupMember::MASTER - end.not_to change { group_with_members.members.count } - - expect(response).to have_http_status(403) - end - - it "returns error if member already exists" do - post api("/groups/#{group_with_members.id}/members", owner), user_id: master.id, access_level: GroupMember::MASTER - expect(response).to have_http_status(409) - end - - it "returns a 400 error when user id is not given" do - post api("/groups/#{group_no_members.id}/members", owner), access_level: GroupMember::MASTER - expect(response).to have_http_status(400) - end - - it "returns a 400 error when access level is not given" do - post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id - expect(response).to have_http_status(400) - end - - it "returns a 422 error when access level is not known" do - post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id, access_level: 1234 - expect(response).to have_http_status(422) - end - end - end - - describe 'PUT /groups/:id/members/:user_id' do - context 'when not a member of the group' do - it 'returns a 409 error if the user is not a group member' do - put( - api("/groups/#{group_no_members.id}/members/#{developer.id}", - owner), access_level: GroupMember::MASTER - ) - expect(response).to have_http_status(404) - end - end - - context 'when a member of the group' do - it 'returns ok and update member access level' do - put( - api("/groups/#{group_with_members.id}/members/#{reporter.id}", - owner), - access_level: GroupMember::MASTER - ) - - expect(response).to have_http_status(200) - - get api("/groups/#{group_with_members.id}/members", owner) - json_reporter = json_response.find do |e| - e['id'] == reporter.id - end - - expect(json_reporter['access_level']).to eq(GroupMember::MASTER) - end - - it 'does not allow guest to modify group members' do - put( - api("/groups/#{group_with_members.id}/members/#{developer.id}", - guest), - access_level: GroupMember::MASTER - ) - - expect(response).to have_http_status(403) - - get api("/groups/#{group_with_members.id}/members", owner) - json_developer = json_response.find do |e| - e['id'] == developer.id - end - - expect(json_developer['access_level']).to eq(GroupMember::DEVELOPER) - end - - it 'returns a 400 error when access level is not given' do - put( - api("/groups/#{group_with_members.id}/members/#{master.id}", owner) - ) - expect(response).to have_http_status(400) - end - - it 'returns a 422 error when access level is not known' do - put( - api("/groups/#{group_with_members.id}/members/#{master.id}", owner), - access_level: 1234 - ) - expect(response).to have_http_status(422) - end - end - end - - describe 'DELETE /groups/:id/members/:user_id' do - context 'when not a member of the group' do - it "does not delete guest's membership of group_with_members" do - random_user = create(:user) - delete api("/groups/#{group_with_members.id}/members/#{owner.id}", random_user) - - expect(response).to have_http_status(404) - end - end - - context "when a member of the group" do - it "deletes guest's membership of group" do - expect do - delete api("/groups/#{group_with_members.id}/members/#{guest.id}", owner) - end.to change { group_with_members.members.count }.by(-1) - - expect(response).to have_http_status(200) - end - - it "returns a 404 error when user id is not known" do - delete api("/groups/#{group_with_members.id}/members/1328", owner) - expect(response).to have_http_status(404) - end - - it "does not allow guest to modify group members" do - delete api("/groups/#{group_with_members.id}/members/#{master.id}", guest) - expect(response).to have_http_status(403) - end - end - end -end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index be2227a72f69b4b63eb9a46e6d2d3c07d1a8d95b..6da79c01624406c58ed600f89078c2cf46369273 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -323,6 +323,24 @@ end end + describe 'GET /internal/merge_request_urls' do + let(:repo_name) { "#{project.namespace.name}/#{project.path}" } + let(:changes) { URI.escape("#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch") } + + before do + project.team << [user, :developer] + get api("/internal/merge_request_urls?project=#{repo_name}&changes=#{changes}"), secret_token: secret_token + end + + it 'returns link to create new merge request' do + expect(json_response).to match [{ + "branch_name" => "new_branch", + "url" => "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch", + "new_merge_request" => true + }] + end + end + def pull(key, project, protocol = 'ssh') post( api("/internal/allowed"), diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a56ee30f7b15eb0bc8b9000cdb14491e568eff35 --- /dev/null +++ b/spec/requests/api/members_spec.rb @@ -0,0 +1,312 @@ +require 'spec_helper' + +describe API::Members, api: true do + include ApiHelpers + + let(:master) { create(:user) } + let(:developer) { create(:user) } + let(:access_requester) { create(:user) } + let(:stranger) { create(:user) } + + let(:project) do + project = create(:project, :public, creator_id: master.id, namespace: master.namespace) + project.team << [developer, :developer] + project.team << [master, :master] + project.request_access(access_requester) + project + end + + let!(:group) do + group = create(:group, :public) + group.add_developer(developer) + group.add_owner(master) + group.request_access(access_requester) + group + end + + shared_examples 'GET /:sources/:id/members' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { get api("/#{source_type.pluralize}/#{source.id}/members", stranger) } + end + + context 'when authenticated as a non-member' do + %i[access_requester stranger].each do |type| + context "as a #{type}" do + it 'returns 200' do + user = public_send(type) + get api("/#{source_type.pluralize}/#{source.id}/members", user) + + expect(response).to have_http_status(200) + expect(json_response.size).to eq(2) + end + end + end + end + + it 'finds members with query string' do + get api("/#{source_type.pluralize}/#{source.id}/members", developer), query: master.username + + expect(response).to have_http_status(200) + expect(json_response.count).to eq(1) + expect(json_response.first['username']).to eq(master.username) + end + end + end + + shared_examples 'GET /:sources/:id/members/:user_id' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { get api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) } + end + + context 'when authenticated as a non-member' do + %i[access_requester stranger].each do |type| + context "as a #{type}" do + it 'returns 200' do + user = public_send(type) + get api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user) + + expect(response).to have_http_status(200) + # User attributes + expect(json_response['id']).to eq(developer.id) + expect(json_response['name']).to eq(developer.name) + expect(json_response['username']).to eq(developer.username) + expect(json_response['state']).to eq(developer.state) + expect(json_response['avatar_url']).to eq(developer.avatar_url) + expect(json_response['web_url']).to eq(Gitlab::Routing.url_helpers.user_url(developer)) + + # Member attributes + expect(json_response['access_level']).to eq(Member::DEVELOPER) + end + end + end + end + end + end + + shared_examples 'POST /:sources/:id/members' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { post api("/#{source_type.pluralize}/#{source.id}/members", stranger) } + end + + context 'when authenticated as a non-member or member with insufficient rights' do + %i[access_requester stranger developer].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + post api("/#{source_type.pluralize}/#{source.id}/members", user) + + expect(response).to have_http_status(403) + end + end + end + end + + context 'when authenticated as a master/owner' do + context 'and new member is already a requester' do + it 'transforms the requester into a proper member' do + expect do + post api("/#{source_type.pluralize}/#{source.id}/members", master), + user_id: access_requester.id, access_level: Member::MASTER + + expect(response).to have_http_status(201) + end.to change { source.members.count }.by(1) + expect(source.requesters.count).to eq(0) + expect(json_response['id']).to eq(access_requester.id) + expect(json_response['access_level']).to eq(Member::MASTER) + end + end + + it 'creates a new member' do + expect do + post api("/#{source_type.pluralize}/#{source.id}/members", master), + user_id: stranger.id, access_level: Member::DEVELOPER + + expect(response).to have_http_status(201) + end.to change { source.members.count }.by(1) + expect(json_response['id']).to eq(stranger.id) + expect(json_response['access_level']).to eq(Member::DEVELOPER) + end + end + + it "returns #{source_type == 'project' ? 201 : 409} if member already exists" do + post api("/#{source_type.pluralize}/#{source.id}/members", master), + user_id: master.id, access_level: Member::MASTER + + expect(response).to have_http_status(source_type == 'project' ? 201 : 409) + end + + it 'returns 400 when user_id is not given' do + post api("/#{source_type.pluralize}/#{source.id}/members", master), + access_level: Member::MASTER + + expect(response).to have_http_status(400) + end + + it 'returns 400 when access_level is not given' do + post api("/#{source_type.pluralize}/#{source.id}/members", master), + user_id: stranger.id + + expect(response).to have_http_status(400) + end + + it 'returns 422 when access_level is not valid' do + post api("/#{source_type.pluralize}/#{source.id}/members", master), + user_id: stranger.id, access_level: 1234 + + expect(response).to have_http_status(422) + end + end + end + + shared_examples 'PUT /:sources/:id/members/:user_id' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) } + end + + context 'when authenticated as a non-member or member with insufficient rights' do + %i[access_requester stranger developer].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user) + + expect(response).to have_http_status(403) + end + end + end + end + + context 'when authenticated as a master/owner' do + it 'updates the member' do + put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master), + access_level: Member::MASTER + + expect(response).to have_http_status(200) + expect(json_response['id']).to eq(developer.id) + expect(json_response['access_level']).to eq(Member::MASTER) + end + end + + it 'returns 409 if member does not exist' do + put api("/#{source_type.pluralize}/#{source.id}/members/123", master), + access_level: Member::MASTER + + expect(response).to have_http_status(404) + end + + it 'returns 400 when access_level is not given' do + put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master) + + expect(response).to have_http_status(400) + end + + it 'returns 422 when access level is not valid' do + put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master), + access_level: 1234 + + expect(response).to have_http_status(422) + end + end + end + + shared_examples 'DELETE /:sources/:id/members/:user_id' do |source_type| + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) } + end + + context 'when authenticated as a non-member or member with insufficient rights' do + %i[access_requester stranger].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user) + + expect(response).to have_http_status(403) + end + end + end + end + + context 'when authenticated as a member and deleting themself' do + it 'deletes the member' do + expect do + delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", developer) + + expect(response).to have_http_status(200) + end.to change { source.members.count }.by(-1) + end + end + + context 'when authenticated as a master/owner' do + context 'and member is a requester' do + it "returns #{source_type == 'project' ? 200 : 404}" do + expect do + delete api("/#{source_type.pluralize}/#{source.id}/members/#{access_requester.id}", master) + + expect(response).to have_http_status(source_type == 'project' ? 200 : 404) + end.not_to change { source.requesters.count } + end + end + + it 'deletes the member' do + expect do + delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master) + + expect(response).to have_http_status(200) + end.to change { source.members.count }.by(-1) + end + end + + it "returns #{source_type == 'project' ? 200 : 404} if member does not exist" do + delete api("/#{source_type.pluralize}/#{source.id}/members/123", master) + + expect(response).to have_http_status(source_type == 'project' ? 200 : 404) + end + end + end + + it_behaves_like 'GET /:sources/:id/members', 'project' do + let(:source) { project } + end + + it_behaves_like 'GET /:sources/:id/members', 'group' do + let(:source) { group } + end + + it_behaves_like 'GET /:sources/:id/members/:user_id', 'project' do + let(:source) { project } + end + + it_behaves_like 'GET /:sources/:id/members/:user_id', 'group' do + let(:source) { group } + end + + it_behaves_like 'POST /:sources/:id/members', 'project' do + let(:source) { project } + end + + it_behaves_like 'POST /:sources/:id/members', 'group' do + let(:source) { group } + end + + it_behaves_like 'PUT /:sources/:id/members/:user_id', 'project' do + let(:source) { project } + end + + it_behaves_like 'PUT /:sources/:id/members/:user_id', 'group' do + let(:source) { group } + end + + it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'project' do + let(:source) { project } + end + + it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'group' do + let(:source) { group } + end +end diff --git a/spec/requests/api/project_members_spec.rb b/spec/requests/api/project_members_spec.rb deleted file mode 100644 index 6a481be1bca927dc9a0b1bc9c2672c177a66e8eb..0000000000000000000000000000000000000000 --- a/spec/requests/api/project_members_spec.rb +++ /dev/null @@ -1,178 +0,0 @@ -require 'spec_helper' - -describe API::API, api: true do - include ApiHelpers - let(:user) { create(:user) } - let(:user2) { create(:user) } - let(:user3) { create(:user) } - let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } - let(:project_member) { create(:project_member, :master, user: user, project: project) } - let(:project_member2) { create(:project_member, :developer, user: user3, project: project) } - - describe "GET /projects/:id/members" do - before { project_member } - before { project_member2 } - - it "returns project team members" do - get api("/projects/#{project.id}/members", user) - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.count).to eq(2) - expect(json_response.map { |u| u['username'] }).to include user.username - end - - it "finds team members with query string" do - get api("/projects/#{project.id}/members", user), query: user.username - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.count).to eq(1) - expect(json_response.first['username']).to eq(user.username) - end - - it "returns a 404 error if id not found" do - get api("/projects/9999/members", user) - expect(response).to have_http_status(404) - end - end - - describe "GET /projects/:id/members/:user_id" do - before { project_member } - - it "returns project team member" do - get api("/projects/#{project.id}/members/#{user.id}", user) - expect(response).to have_http_status(200) - expect(json_response['username']).to eq(user.username) - expect(json_response['access_level']).to eq(ProjectMember::MASTER) - end - - it "returns a 404 error if user id not found" do - get api("/projects/#{project.id}/members/1234", user) - expect(response).to have_http_status(404) - end - end - - describe "POST /projects/:id/members" do - it "adds user to project team" do - expect do - post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: ProjectMember::DEVELOPER - end.to change { ProjectMember.count }.by(1) - - expect(response).to have_http_status(201) - expect(json_response['username']).to eq(user2.username) - expect(json_response['access_level']).to eq(ProjectMember::DEVELOPER) - end - - it "returns a 201 status if user is already project member" do - post api("/projects/#{project.id}/members", user), - user_id: user2.id, - access_level: ProjectMember::DEVELOPER - expect do - post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: ProjectMember::DEVELOPER - end.not_to change { ProjectMember.count } - - expect(response).to have_http_status(201) - expect(json_response['username']).to eq(user2.username) - expect(json_response['access_level']).to eq(ProjectMember::DEVELOPER) - end - - it "returns a 400 error when user id is not given" do - post api("/projects/#{project.id}/members", user), access_level: ProjectMember::MASTER - expect(response).to have_http_status(400) - end - - it "returns a 400 error when access level is not given" do - post api("/projects/#{project.id}/members", user), user_id: user2.id - expect(response).to have_http_status(400) - end - - it "returns a 422 error when access level is not known" do - post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: 1234 - expect(response).to have_http_status(422) - end - - context 'project in a group' do - before do - project2 = create(:project, group: create(:group, membership_lock: true)) - project2.group.add_owner(user) - post api("/projects/#{project2.id}/members", user), user_id: user2.id, access_level: ProjectMember::MASTER - end - - it 'returns a 405 method not allowed error when group membership lock is enabled' do - expect(response.status).to eq 405 - end - end - end - - describe "PUT /projects/:id/members/:user_id" do - before { project_member2 } - - it "updates project team member" do - put api("/projects/#{project.id}/members/#{user3.id}", user), access_level: ProjectMember::MASTER - expect(response).to have_http_status(200) - expect(json_response['username']).to eq(user3.username) - expect(json_response['access_level']).to eq(ProjectMember::MASTER) - end - - it "returns a 404 error if user_id is not found" do - put api("/projects/#{project.id}/members/1234", user), access_level: ProjectMember::MASTER - expect(response).to have_http_status(404) - end - - it "returns a 400 error when access level is not given" do - put api("/projects/#{project.id}/members/#{user3.id}", user) - expect(response).to have_http_status(400) - end - - it "returns a 422 error when access level is not known" do - put api("/projects/#{project.id}/members/#{user3.id}", user), access_level: 123 - expect(response).to have_http_status(422) - end - end - - describe "DELETE /projects/:id/members/:user_id" do - before do - project_member - project_member2 - end - - it "removes user from project team" do - expect do - delete api("/projects/#{project.id}/members/#{user3.id}", user) - end.to change { ProjectMember.count }.by(-1) - end - - it "returns 200 if team member is not part of a project" do - delete api("/projects/#{project.id}/members/#{user3.id}", user) - expect do - delete api("/projects/#{project.id}/members/#{user3.id}", user) - end.not_to change { ProjectMember.count } - expect(response).to have_http_status(200) - end - - it "returns 200 if team member already removed" do - delete api("/projects/#{project.id}/members/#{user3.id}", user) - delete api("/projects/#{project.id}/members/#{user3.id}", user) - expect(response).to have_http_status(200) - end - - it "returns 200 OK when the user was not member" do - expect do - delete api("/projects/#{project.id}/members/1000000", user) - end.to change { ProjectMember.count }.by(0) - expect(response).to have_http_status(200) - expect(json_response['id']).to eq(1000000) - expect(json_response['message']).to eq('Access revoked') - end - - context 'when the user is not an admin or owner' do - it 'can leave the project' do - expect do - delete api("/projects/#{project.id}/members/#{user3.id}", user3) - end.to change { ProjectMember.count }.by(-1) - - expect(response).to have_http_status(200) - expect(json_response['id']).to eq(project_member2.id) - end - end - end -end diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index 5702682fc7da2f3eddc97cae69a940fbef18c783..82bba1ce8a40fd8bc94c13b235de6d64d8ff10da 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -50,7 +50,8 @@ post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'master') expect(response).to have_http_status(201) pipeline.builds.reload - expect(pipeline.builds.size).to eq(2) + expect(pipeline.builds.pending.size).to eq(2) + expect(pipeline.builds.size).to eq(5) end it 'returns bad request with no builds created if there\'s no commit for that ref' do diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 85d1751d17b22ac56ae61ec7abcaa4cfbb25e589..2eca46d787ef5c4686db102de6e462b935568698 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -410,9 +410,9 @@ end.to change{ user.keys.count }.by(1) end - it "returns 405 for invalid ID" do - post api("/users/ASDF/keys", admin) - expect(response).to have_http_status(405) + it "returns 400 for invalid ID" do + post api("/users/999999/keys", admin) + expect(response).to have_http_status(400) end end @@ -441,11 +441,6 @@ expect(json_response).to be_an Array expect(json_response.first['title']).to eq(key.title) end - - it "returns 405 for invalid ID" do - get api("/users/ASDF/keys", admin) - expect(response).to have_http_status(405) - end end end @@ -502,8 +497,8 @@ end it "raises error for invalid ID" do - post api("/users/ASDF/emails", admin) - expect(response).to have_http_status(405) + post api("/users/999999/emails", admin) + expect(response).to have_http_status(400) end end diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 8ff29728a2c7f1c2a0bc1eb3ebd4e3320b91c192..3c0ace1ac5c505f3f47014ff964912c6f6a062f6 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -6,112 +6,102 @@ let(:runner) { FactoryGirl.create(:ci_runner, tag_list: ["mysql", "ruby"]) } let(:project) { FactoryGirl.create(:empty_project) } - before do - stub_ci_pipeline_to_return_yaml_file - end - describe "Builds API for runners" do - let(:shared_runner) { FactoryGirl.create(:ci_runner, token: "SharedRunner") } - let(:shared_project) { FactoryGirl.create(:empty_project, name: "SharedProject") } + let(:pipeline) { create(:ci_pipeline_without_jobs, project: project, ref: 'master') } before do - FactoryGirl.create :ci_runner_project, project: project, runner: runner + project.runners << runner end describe "POST /builds/register" do - it "starts a build" do - pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master') - pipeline.create_builds(nil) - build = pipeline.builds.first + let!(:build) { create(:ci_build, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } - post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } + it "starts a build" do + register_builds info: { platform: :darwin } expect(response).to have_http_status(201) expect(json_response['sha']).to eq(build.sha) expect(runner.reload.platform).to eq("darwin") + expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] }) + expect(json_response["variables"]).to include( + { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true }, + { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true }, + { "key" => "DB_NAME", "value" => "postgres", "public" => true } + ) end - it "returns 404 error if no pending build found" do - post ci_api("/builds/register"), token: runner.token - - expect(response).to have_http_status(404) - end - - it "returns 404 error if no builds for specific runner" do - pipeline = FactoryGirl.create(:ci_pipeline, project: shared_project) - FactoryGirl.create(:ci_build, pipeline: pipeline, status: 'pending') + context 'when builds are finished' do + before do + build.success + end - post ci_api("/builds/register"), token: runner.token + it "returns 404 error if no builds for specific runner" do + register_builds - expect(response).to have_http_status(404) + expect(response).to have_http_status(404) + end end - it "returns 404 error if no builds for shared runner" do - pipeline = FactoryGirl.create(:ci_pipeline, project: project) - FactoryGirl.create(:ci_build, pipeline: pipeline, status: 'pending') + context 'for other project with builds' do + before do + build.success + create(:ci_build, :pending) + end - post ci_api("/builds/register"), token: shared_runner.token + it "returns 404 error if no builds for shared runner" do + register_builds - expect(response).to have_http_status(404) + expect(response).to have_http_status(404) + end end - it "returns options" do - pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master') - pipeline.create_builds(nil) + context 'for shared runner' do + let(:shared_runner) { create(:ci_runner, token: "SharedRunner") } - post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } + it "should return 404 error if no builds for shared runner" do + register_builds shared_runner.token - expect(response).to have_http_status(201) - expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] }) + expect(response).to have_http_status(404) + end end - it "returns variables" do - pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master') - pipeline.create_builds(nil) - project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") - - post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } + context 'for triggered build' do + before do + trigger = create(:ci_trigger, project: project) + create(:ci_trigger_request_with_variables, pipeline: pipeline, builds: [build], trigger: trigger) + project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") + end - expect(response).to have_http_status(201) - expect(json_response["variables"]).to include( - { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true }, - { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true }, - { "key" => "DB_NAME", "value" => "postgres", "public" => true }, - { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false } - ) + it "returns variables for triggers" do + register_builds info: { platform: :darwin } + + expect(response).to have_http_status(201) + expect(json_response["variables"]).to include( + { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true }, + { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true }, + { "key" => "CI_BUILD_TRIGGERED", "value" => "true", "public" => true }, + { "key" => "DB_NAME", "value" => "postgres", "public" => true }, + { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false }, + { "key" => "TRIGGER_KEY_1", "value" => "TRIGGER_VALUE_1", "public" => false }, + ) + end end - it "returns variables for triggers" do - trigger = FactoryGirl.create(:ci_trigger, project: project) - pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master') - - trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) - pipeline.create_builds(nil, trigger_request) - project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") - - post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } - - expect(response).to have_http_status(201) - expect(json_response["variables"]).to include( - { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true }, - { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true }, - { "key" => "CI_BUILD_TRIGGERED", "value" => "true", "public" => true }, - { "key" => "DB_NAME", "value" => "postgres", "public" => true }, - { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false }, - { "key" => "TRIGGER_KEY_1", "value" => "TRIGGER_VALUE_1", "public" => false } - ) - end + context 'with multiple builds' do + before do + build.success + end - it "returns dependent builds" do - pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master') - pipeline.create_builds(nil, nil) - pipeline.builds.where(stage: 'test').each(&:success) + let!(:test_build) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) } - post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } + it "returns dependent builds" do + register_builds info: { platform: :darwin } - expect(response).to have_http_status(201) - expect(json_response["depends_on_builds"].count).to eq(2) - expect(json_response["depends_on_builds"][0]["name"]).to eq("rspec") + expect(response).to have_http_status(201) + expect(json_response["id"]).to eq(test_build.id) + expect(json_response["depends_on_builds"].count).to eq(1) + expect(json_response["depends_on_builds"][0]).to include('id' => build.id, 'name' => 'spinach') + end end %w(name version revision platform architecture).each do |param| @@ -121,8 +111,9 @@ subject { runner.read_attribute(param.to_sym) } it do - post ci_api("/builds/register"), token: runner.token, info: { param => value } - expect(response).to have_http_status(404) + register_builds info: { param => value } + + expect(response).to have_http_status(201) runner.reload is_expected.to eq(value) end @@ -131,8 +122,7 @@ context 'when build has no tags' do before do - pipeline = create(:ci_pipeline, project: project) - create(:ci_build, pipeline: pipeline, tags: []) + build.update(tags: []) end context 'when runner is allowed to pick untagged builds' do @@ -154,17 +144,15 @@ expect(response).to have_http_status 404 end end + end - def register_builds - post ci_api("/builds/register"), token: runner.token, - info: { platform: :darwin } - end + def register_builds(token = runner.token, **params) + post ci_api("/builds/register"), params.merge(token: token) end end describe "PUT /builds/:id" do - let(:pipeline) {create(:ci_pipeline, project: project)} - let(:build) { create(:ci_build, :trace, pipeline: pipeline, runner_id: runner.id) } + let(:build) { create(:ci_build, :pending, :trace, pipeline: pipeline, runner_id: runner.id) } before do build.run! @@ -189,7 +177,7 @@ def register_builds end describe 'PATCH /builds/:id/trace.txt' do - let(:build) { create(:ci_build, :trace, runner_id: runner.id) } + let(:build) { create(:ci_build, :pending, :trace, runner_id: runner.id) } let(:headers) { { Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token, 'Content-Type' => 'text/plain' } } let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) } @@ -237,8 +225,7 @@ def register_builds context "Artifacts" do let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') } - let(:pipeline) { create(:ci_pipeline, project: project) } - let(:build) { create(:ci_build, pipeline: pipeline, runner_id: runner.id) } + let(:build) { create(:ci_build, :pending, pipeline: pipeline, runner_id: runner.id) } let(:authorize_url) { ci_api("/builds/#{build.id}/artifacts/authorize") } let(:post_url) { ci_api("/builds/#{build.id}/artifacts") } let(:delete_url) { ci_api("/builds/#{build.id}/artifacts") } diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb index 3312bd1166984792289fbc1c26eb7ba6e961d3fb..0a0f979f57d659e57acb9a1748e614ad88f1f90f 100644 --- a/spec/requests/ci/api/triggers_spec.rb +++ b/spec/requests/ci/api/triggers_spec.rb @@ -42,7 +42,8 @@ post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options expect(response).to have_http_status(201) pipeline.builds.reload - expect(pipeline.builds.size).to eq(2) + expect(pipeline.builds.pending.size).to eq(2) + expect(pipeline.builds.size).to eq(5) end it 'returns bad request with no builds created if there\'s no commit for that ref' do diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 3441105d315d156dc1d0ea97a2825f75b4eff2b4..374f8046b1f096e502d0b819a6956677004905ad 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -75,9 +75,9 @@ context "with correct credentials" do let(:env) { { user: user.username, password: user.password } } - it "uploads get status 200 (because Push rules do the real check)" do + it "uploads get status 403" do upload(path, env) do |response| - expect(response).to have_http_status(200) + expect(response).to have_http_status(403) end end @@ -86,7 +86,7 @@ allow(Gitlab.config.gitlab_shell).to receive(:receive_pack).and_return(false) upload(path, env) do |response| - expect(response).to have_http_status(404) + expect(response).to have_http_status(403) end end end @@ -324,9 +324,9 @@ def attempt_login(include_password) end end - it "uploads get status 200 (because Push rules do the real check)" do + it "uploads get status 404" do upload(path, user: user.username, password: user.password) do |response| - expect(response).to have_http_status(200) + expect(response).to have_http_status(404) end end end diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index 93d2bc160cc8fc3ca3abcc52b3c773417a24c0b5..4c9b4a8ba422ef6f921b5130557ca82628bcdec2 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Lfs::Router do +describe 'Git LFS API and storage' do let(:user) { create(:user) } let!(:lfs_object) { create(:lfs_object, :with_file) } @@ -31,10 +31,11 @@ 'operation' => 'upload' } end + let(:authorization) { authorize_user } before do allow(Gitlab.config.lfs).to receive(:enabled).and_return(false) - post_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers + post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers end it 'responds with 501' do @@ -71,8 +72,9 @@ end context 'when handling lfs request using deprecated API' do + let(:authorization) { authorize_user } before do - post_json "#{project.http_url_to_repo}/info/lfs/objects", nil, headers + post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects", nil, headers end it_behaves_like 'a deprecated' @@ -118,8 +120,8 @@ project.lfs_objects << lfs_object end - it 'responds with status 403' do - expect(response).to have_http_status(403) + it 'responds with status 404' do + expect(response).to have_http_status(404) end end @@ -147,8 +149,8 @@ context 'without required headers' do let(:authorization) { authorize_user } - it 'responds with status 403' do - expect(response).to have_http_status(403) + it 'responds with status 404' do + expect(response).to have_http_status(404) end end end @@ -162,7 +164,7 @@ enable_lfs update_lfs_permissions update_user_permissions - post_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers + post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers end describe 'download' do @@ -304,10 +306,10 @@ end context 'when user does is not member of the project' do - let(:role) { :guest } + let(:update_user_permissions) { nil } - it 'responds with 403' do - expect(response).to have_http_status(403) + it 'responds with 404' do + expect(response).to have_http_status(404) end end @@ -510,6 +512,7 @@ describe 'unsupported' do let(:project) { create(:empty_project) } + let(:authorization) { authorize_user } let(:body) do { 'operation' => 'other', 'objects' => [ @@ -553,11 +556,11 @@ context 'and request is sent with a malformed headers' do before do - put_finalize('cat /etc/passwd') + put_finalize('/etc/passwd') end it 'does not recognize it as a valid lfs command' do - expect(response).to have_http_status(403) + expect(response).to have_http_status(401) end end end @@ -582,6 +585,16 @@ expect(response).to have_http_status(403) end end + + context 'and request is sent with a malformed headers' do + before do + put_finalize('/etc/passwd') + end + + it 'does not recognize it as a valid lfs command' do + expect(response).to have_http_status(403) + end + end end describe 'to one project' do @@ -624,9 +637,25 @@ expect(lfs_object.projects.pluck(:id)).to include(project.id) end end + + context 'invalid tempfiles' do + it 'rejects slashes in the tempfile name (path traversal' do + put_finalize('foo/bar') + expect(response).to have_http_status(403) + end + + it 'rejects tempfile names that do not start with the oid' do + put_finalize("foo#{sample_oid}") + expect(response).to have_http_status(403) + end + end end describe 'and user does not have push access' do + before do + project.team << [user, :reporter] + end + it_behaves_like 'forbidden' end end @@ -758,8 +787,8 @@ def fork_project(project, user, object = nil) Projects::ForkService.new(project, user, {}).execute end - def post_json(url, body = nil, headers = nil) - post(url, body.try(:to_json), (headers || {}).merge('Content-Type' => 'application/json')) + def post_lfs_json(url, body = nil, headers = nil) + post(url, body.try(:to_json), (headers || {}).merge('Content-Type' => 'application/vnd.git-lfs+json')) end def json_response diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index b941e78f983539666eb04cb5370fbb5bc67ce3f3..77842057a1044917fb1a61a1ef406ea8f044fe5a 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -60,7 +60,7 @@ # project GET /:id(.:format) projects#show # PUT /:id(.:format) projects#update # DELETE /:id(.:format) projects#destroy -# markdown_preview_project POST /:id/markdown_preview(.:format) projects#markdown_preview +# preview_markdown_project POST /:id/preview_markdown(.:format) projects#preview_markdown describe ProjectsController, 'routing' do it 'to #create' do expect(post('/projects')).to route_to('projects#create') @@ -91,9 +91,9 @@ expect(delete('/gitlab/gitlabhq')).to route_to('projects#destroy', namespace_id: 'gitlab', id: 'gitlabhq') end - it 'to #markdown_preview' do - expect(post('/gitlab/gitlabhq/markdown_preview')).to( - route_to('projects#markdown_preview', namespace_id: 'gitlab', id: 'gitlabhq') + it 'to #preview_markdown' do + expect(post('/gitlab/gitlabhq/preview_markdown')).to( + route_to('projects#preview_markdown', namespace_id: 'gitlab', id: 'gitlabhq') ) end end diff --git a/spec/services/ci/create_builds_service_spec.rb b/spec/services/ci/create_builds_service_spec.rb deleted file mode 100644 index 8b0becd83d3805ceb6a47ae87cea2c26fb576455..0000000000000000000000000000000000000000 --- a/spec/services/ci/create_builds_service_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -require 'spec_helper' - -describe Ci::CreateBuildsService, services: true do - let(:pipeline) { create(:ci_pipeline, ref: 'master') } - let(:user) { create(:user) } - - describe '#execute' do - # Using stubbed .gitlab-ci.yml created in commit factory - # - - subject do - described_class.new(pipeline).execute('test', user, status, nil) - end - - context 'next builds available' do - let(:status) { 'success' } - - it { is_expected.to be_an_instance_of Array } - it { is_expected.to all(be_an_instance_of Ci::Build) } - - it 'does not persist created builds' do - expect(subject.first).not_to be_persisted - end - end - - context 'builds skipped' do - let(:status) { 'skipped' } - - it { is_expected.to be_empty } - end - end -end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..4aadd009f3ecf0316e6ae28b9de734f317a29e69 --- /dev/null +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -0,0 +1,214 @@ +require 'spec_helper' + +describe Ci::CreatePipelineService, services: true do + let(:project) { FactoryGirl.create(:project) } + let(:user) { create(:admin) } + + before do + stub_ci_pipeline_to_return_yaml_file + end + + describe '#execute' do + def execute(params) + described_class.new(project, user, params).execute + end + + context 'valid params' do + let(:pipeline) do + execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: [{ message: "Message" }]) + end + + it { expect(pipeline).to be_kind_of(Ci::Pipeline) } + it { expect(pipeline).to be_valid } + it { expect(pipeline).to be_persisted } + it { expect(pipeline).to eq(project.pipelines.last) } + it { expect(pipeline).to have_attributes(user: user) } + it { expect(pipeline.builds.first).to be_kind_of(Ci::Build) } + end + + context "skip tag if there is no build for it" do + it "creates commit if there is appropriate job" do + result = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: [{ message: "Message" }]) + expect(result).to be_persisted + end + + it "creates commit if there is no appropriate job but deploy job has right ref setting" do + config = YAML.dump({ deploy: { script: "ls", only: ["master"] } }) + stub_ci_pipeline_yaml_file(config) + result = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: [{ message: "Message" }]) + + expect(result).to be_persisted + end + end + + it 'skips creating pipeline for refs without .gitlab-ci.yml' do + stub_ci_pipeline_yaml_file(nil) + result = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: [{ message: 'Message' }]) + + expect(result).not_to be_persisted + expect(Ci::Pipeline.count).to eq(0) + end + + it 'fails commits if yaml is invalid' do + message = 'message' + allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message } + stub_ci_pipeline_yaml_file('invalid: file: file') + commits = [{ message: message }] + pipeline = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: commits) + + expect(pipeline).to be_persisted + expect(pipeline.builds.any?).to be false + expect(pipeline.status).to eq('failed') + expect(pipeline.yaml_errors).not_to be_nil + end + + context 'when commit contains a [ci skip] directive' do + let(:message) { "some message[ci skip]" } + let(:messageFlip) { "some message[skip ci]" } + let(:capMessage) { "some message[CI SKIP]" } + let(:capMessageFlip) { "some message[SKIP CI]" } + + before do + allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message } + end + + it "skips builds creation if there is [ci skip] tag in commit message" do + commits = [{ message: message }] + pipeline = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: commits) + + expect(pipeline).to be_persisted + expect(pipeline.builds.any?).to be false + expect(pipeline.status).to eq("skipped") + end + + it "skips builds creation if there is [skip ci] tag in commit message" do + commits = [{ message: messageFlip }] + pipeline = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: commits) + + expect(pipeline).to be_persisted + expect(pipeline.builds.any?).to be false + expect(pipeline.status).to eq("skipped") + end + + it "skips builds creation if there is [CI SKIP] tag in commit message" do + commits = [{ message: capMessage }] + pipeline = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: commits) + + expect(pipeline).to be_persisted + expect(pipeline.builds.any?).to be false + expect(pipeline.status).to eq("skipped") + end + + it "skips builds creation if there is [SKIP CI] tag in commit message" do + commits = [{ message: capMessageFlip }] + pipeline = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: commits) + + expect(pipeline).to be_persisted + expect(pipeline.builds.any?).to be false + expect(pipeline.status).to eq("skipped") + end + + it "does not skips builds creation if there is no [ci skip] or [skip ci] tag in commit message" do + allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" } + + commits = [{ message: "some message" }] + pipeline = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: commits) + + expect(pipeline).to be_persisted + expect(pipeline.builds.first.name).to eq("rspec") + end + + it "fails builds creation if there is [ci skip] tag in commit message and yaml is invalid" do + stub_ci_pipeline_yaml_file('invalid: file: fiile') + commits = [{ message: message }] + pipeline = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: commits) + + expect(pipeline).to be_persisted + expect(pipeline.builds.any?).to be false + expect(pipeline.status).to eq("failed") + expect(pipeline.yaml_errors).not_to be_nil + end + end + + it "creates commit with failed status if yaml is invalid" do + stub_ci_pipeline_yaml_file('invalid: file') + commits = [{ message: "some message" }] + pipeline = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: commits) + + expect(pipeline).to be_persisted + expect(pipeline.status).to eq("failed") + expect(pipeline.builds.any?).to be false + end + + context 'when there are no jobs for this pipeline' do + before do + config = YAML.dump({ test: { script: 'ls', only: ['feature'] } }) + stub_ci_pipeline_yaml_file(config) + end + + it 'does not create a new pipeline' do + result = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: [{ message: 'some msg' }]) + + expect(result).not_to be_persisted + expect(Ci::Build.all).to be_empty + expect(Ci::Pipeline.count).to eq(0) + end + end + + context 'with manual actions' do + before do + config = YAML.dump({ deploy: { script: 'ls', when: 'manual' } }) + stub_ci_pipeline_yaml_file(config) + end + + it 'does not create a new pipeline' do + result = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: [{ message: 'some msg' }]) + + expect(result).to be_persisted + expect(result.manual_actions).not_to be_empty + end + end + end +end diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb index b72e0bd3dbeb4708fda3c6481c1923562aadbd0b..d8c443d29d5646d8ee788f8aad5f6f55262d89d5 100644 --- a/spec/services/ci/create_trigger_request_service_spec.rb +++ b/spec/services/ci/create_trigger_request_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Ci::CreateTriggerRequestService, services: true do - let(:service) { Ci::CreateTriggerRequestService.new } + let(:service) { described_class.new } let(:project) { create(:project) } let(:trigger) { create(:ci_trigger, project: project) } @@ -27,8 +27,7 @@ subject { service.execute(project, trigger, 'master') } before do - stub_ci_pipeline_yaml_file('{}') - FactoryGirl.create :ci_pipeline, project: project + stub_ci_pipeline_yaml_file('script: { only: [develop], script: hello World }') end it { expect(subject).to be_nil } diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb index 3a3e3efe709ada4756d963b511f126560017fc6a..c931c3e4829d2e32bdf5bc0a55ae967fc69c2887 100644 --- a/spec/services/ci/image_for_build_service_spec.rb +++ b/spec/services/ci/image_for_build_service_spec.rb @@ -5,8 +5,8 @@ module Ci let(:service) { ImageForBuildService.new } let(:project) { FactoryGirl.create(:empty_project) } let(:commit_sha) { '01234567890123456789' } - let(:commit) { project.ensure_pipeline(commit_sha, 'master') } - let(:build) { FactoryGirl.create(:ci_build, pipeline: commit) } + let(:pipeline) { project.ensure_pipeline(commit_sha, 'master') } + let(:build) { FactoryGirl.create(:ci_build, pipeline: pipeline) } describe '#execute' do before { build } diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..ad8c2485888d0cf6955662737bbb710ca5c544d9 --- /dev/null +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -0,0 +1,288 @@ +require 'spec_helper' + +describe Ci::ProcessPipelineService, services: true do + let(:pipeline) { create(:ci_pipeline, ref: 'master') } + let(:user) { create(:user) } + let(:all_builds) { pipeline.builds } + let(:builds) { all_builds.where.not(status: [:created, :skipped]) } + let(:config) { nil } + + before do + allow(pipeline).to receive(:ci_yaml_file).and_return(config) + end + + describe '#execute' do + def create_builds + described_class.new(pipeline.project, user).execute(pipeline) + end + + def succeed_pending + builds.pending.update_all(status: 'success') + end + + context 'start queuing next builds' do + before do + create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage_idx: 0) + create(:ci_build, :created, pipeline: pipeline, name: 'mac', stage_idx: 0) + create(:ci_build, :created, pipeline: pipeline, name: 'rspec', stage_idx: 1) + create(:ci_build, :created, pipeline: pipeline, name: 'rubocop', stage_idx: 1) + create(:ci_build, :created, pipeline: pipeline, name: 'deploy', stage_idx: 2) + end + + it 'processes a pipeline' do + expect(create_builds).to be_truthy + succeed_pending + expect(builds.success.count).to eq(2) + + expect(create_builds).to be_truthy + succeed_pending + expect(builds.success.count).to eq(4) + + expect(create_builds).to be_truthy + succeed_pending + expect(builds.success.count).to eq(5) + + expect(create_builds).to be_falsey + end + + it 'does not process pipeline if existing stage is running' do + expect(create_builds).to be_truthy + expect(builds.pending.count).to eq(2) + + expect(create_builds).to be_falsey + expect(builds.pending.count).to eq(2) + end + end + + context 'custom stage with first job allowed to fail' do + before do + create(:ci_build, :created, pipeline: pipeline, name: 'clean_job', stage_idx: 0, allow_failure: true) + create(:ci_build, :created, pipeline: pipeline, name: 'test_job', stage_idx: 1, allow_failure: true) + end + + it 'automatically triggers a next stage when build finishes' do + expect(create_builds).to be_truthy + expect(builds.pluck(:status)).to contain_exactly('pending') + + pipeline.builds.running_or_pending.each(&:drop) + expect(builds.pluck(:status)).to contain_exactly('failed', 'pending') + end + end + + context 'properly creates builds when "when" is defined' do + before do + create(:ci_build, :created, pipeline: pipeline, name: 'build', stage_idx: 0) + create(:ci_build, :created, pipeline: pipeline, name: 'test', stage_idx: 1) + create(:ci_build, :created, pipeline: pipeline, name: 'test_failure', stage_idx: 2, when: 'on_failure') + create(:ci_build, :created, pipeline: pipeline, name: 'deploy', stage_idx: 3) + create(:ci_build, :created, pipeline: pipeline, name: 'production', stage_idx: 3, when: 'manual') + create(:ci_build, :created, pipeline: pipeline, name: 'cleanup', stage_idx: 4, when: 'always') + create(:ci_build, :created, pipeline: pipeline, name: 'clear cache', stage_idx: 4, when: 'manual') + end + + context 'when builds are successful' do + it 'properly creates builds' do + expect(create_builds).to be_truthy + expect(builds.pluck(:name)).to contain_exactly('build') + expect(builds.pluck(:status)).to contain_exactly('pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test') + expect(builds.pluck(:status)).to contain_exactly('success', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy') + expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup') + expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success') + pipeline.reload + expect(pipeline.status).to eq('success') + end + end + + context 'when test job fails' do + it 'properly creates builds' do + expect(create_builds).to be_truthy + expect(builds.pluck(:name)).to contain_exactly('build') + expect(builds.pluck(:status)).to contain_exactly('pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test') + expect(builds.pluck(:status)).to contain_exactly('success', 'pending') + pipeline.builds.running_or_pending.each(&:drop) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure') + expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') + expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success') + pipeline.reload + expect(pipeline.status).to eq('failed') + end + end + + context 'when test and test_failure jobs fail' do + it 'properly creates builds' do + expect(create_builds).to be_truthy + expect(builds.pluck(:name)).to contain_exactly('build') + expect(builds.pluck(:status)).to contain_exactly('pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test') + expect(builds.pluck(:status)).to contain_exactly('success', 'pending') + pipeline.builds.running_or_pending.each(&:drop) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure') + expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending') + pipeline.builds.running_or_pending.each(&:drop) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') + expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') + expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success') + pipeline.reload + expect(pipeline.status).to eq('failed') + end + end + + context 'when deploy job fails' do + it 'properly creates builds' do + expect(create_builds).to be_truthy + expect(builds.pluck(:name)).to contain_exactly('build') + expect(builds.pluck(:status)).to contain_exactly('pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test') + expect(builds.pluck(:status)).to contain_exactly('success', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy') + expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'pending') + pipeline.builds.running_or_pending.each(&:drop) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup') + expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success') + pipeline.reload + expect(pipeline.status).to eq('failed') + end + end + + context 'when build is canceled in the second stage' do + it 'does not schedule builds after build has been canceled' do + expect(create_builds).to be_truthy + expect(builds.pluck(:name)).to contain_exactly('build') + expect(builds.pluck(:status)).to contain_exactly('pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.running_or_pending).not_to be_empty + + expect(builds.pluck(:name)).to contain_exactly('build', 'test') + expect(builds.pluck(:status)).to contain_exactly('success', 'pending') + pipeline.builds.running_or_pending.each(&:cancel) + + expect(builds.running_or_pending).to be_empty + expect(pipeline.reload.status).to eq('canceled') + end + end + + context 'when listing manual actions' do + it 'returns only for skipped builds' do + # currently all builds are created + expect(create_builds).to be_truthy + expect(manual_actions).to be_empty + + # succeed stage build + pipeline.builds.running_or_pending.each(&:success) + expect(manual_actions).to be_empty + + # succeed stage test + pipeline.builds.running_or_pending.each(&:success) + expect(manual_actions).to be_one # production + + # succeed stage deploy + pipeline.builds.running_or_pending.each(&:success) + expect(manual_actions).to be_many # production and clear cache + end + + def manual_actions + pipeline.manual_actions + end + end + end + + context 'creates a builds from .gitlab-ci.yml' do + let(:config) do + YAML.dump({ + rspec: { + stage: 'test', + script: 'rspec' + }, + rubocop: { + stage: 'test', + script: 'rubocop' + }, + deploy: { + stage: 'deploy', + script: 'deploy' + } + }) + end + + # Using stubbed .gitlab-ci.yml created in commit factory + # + + before do + stub_ci_pipeline_yaml_file(config) + create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage: 'build', stage_idx: 0) + create(:ci_build, :created, pipeline: pipeline, name: 'mac', stage: 'build', stage_idx: 0) + end + + it 'when processing a pipeline' do + # Currently we have two builds with state created + expect(builds.count).to eq(0) + expect(all_builds.count).to eq(2) + + # Create builds will mark the created as pending + expect(create_builds).to be_truthy + expect(builds.count).to eq(2) + expect(all_builds.count).to eq(2) + + # When we builds succeed we will create a rest of pipeline from .gitlab-ci.yml + # We will have 2 succeeded, 2 pending (from stage test), total 5 (one more build from deploy) + succeed_pending + expect(create_builds).to be_truthy + expect(builds.success.count).to eq(2) + expect(builds.pending.count).to eq(2) + expect(all_builds.count).to eq(5) + + # When we succeed the 2 pending from stage test, + # We will queue a deploy stage, no new builds will be created + succeed_pending + expect(create_builds).to be_truthy + expect(builds.pending.count).to eq(1) + expect(builds.success.count).to eq(4) + expect(all_builds.count).to eq(5) + + # When we succeed last pending build, we will have a total of 5 succeeded builds, no new builds will be created + succeed_pending + expect(create_builds).to be_falsey + expect(builds.success.count).to eq(5) + expect(all_builds.count).to eq(5) + end + end + end +end diff --git a/spec/services/create_commit_builds_service_spec.rb b/spec/services/create_commit_builds_service_spec.rb deleted file mode 100644 index d4c5e584421627b21d1a08fc59d975bf0aa6d4b4..0000000000000000000000000000000000000000 --- a/spec/services/create_commit_builds_service_spec.rb +++ /dev/null @@ -1,241 +0,0 @@ -require 'spec_helper' - -describe CreateCommitBuildsService, services: true do - let(:service) { CreateCommitBuildsService.new } - let(:project) { FactoryGirl.create(:empty_project) } - let(:user) { create(:user) } - - before do - stub_ci_pipeline_to_return_yaml_file - end - - describe '#execute' do - context 'valid params' do - let(:pipeline) do - service.execute(project, user, - ref: 'refs/heads/master', - before: '00000000', - after: '31das312', - commits: [{ message: "Message" }] - ) - end - - it { expect(pipeline).to be_kind_of(Ci::Pipeline) } - it { expect(pipeline).to be_valid } - it { expect(pipeline).to be_persisted } - it { expect(pipeline).to eq(project.pipelines.last) } - it { expect(pipeline).to have_attributes(user: user) } - it { expect(pipeline.builds.first).to be_kind_of(Ci::Build) } - end - - context "skip tag if there is no build for it" do - it "creates commit if there is appropriate job" do - result = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: [{ message: "Message" }] - ) - expect(result).to be_persisted - end - - it "creates commit if there is no appropriate job but deploy job has right ref setting" do - config = YAML.dump({ deploy: { script: "ls", only: ["0_1"] } }) - stub_ci_pipeline_yaml_file(config) - - result = service.execute(project, user, - ref: 'refs/heads/0_1', - before: '00000000', - after: '31das312', - commits: [{ message: "Message" }] - ) - expect(result).to be_persisted - end - end - - it 'skips creating pipeline for refs without .gitlab-ci.yml' do - stub_ci_pipeline_yaml_file(nil) - result = service.execute(project, user, - ref: 'refs/heads/0_1', - before: '00000000', - after: '31das312', - commits: [{ message: 'Message' }] - ) - expect(result).to be_falsey - expect(Ci::Pipeline.count).to eq(0) - end - - it 'fails commits if yaml is invalid' do - message = 'message' - allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message } - stub_ci_pipeline_yaml_file('invalid: file: file') - commits = [{ message: message }] - pipeline = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - expect(pipeline).to be_persisted - expect(pipeline.builds.any?).to be false - expect(pipeline.status).to eq('failed') - expect(pipeline.yaml_errors).not_to be_nil - end - - context 'when commit contains a [ci skip] directive' do - let(:message) { "some message[ci skip]" } - let(:messageFlip) { "some message[skip ci]" } - let(:capMessage) { "some message[CI SKIP]" } - let(:capMessageFlip) { "some message[SKIP CI]" } - - before do - allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message } - end - - it "skips builds creation if there is [ci skip] tag in commit message" do - commits = [{ message: message }] - pipeline = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - - expect(pipeline).to be_persisted - expect(pipeline.builds.any?).to be false - expect(pipeline.status).to eq("skipped") - end - - it "skips builds creation if there is [skip ci] tag in commit message" do - commits = [{ message: messageFlip }] - pipeline = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - - expect(pipeline).to be_persisted - expect(pipeline.builds.any?).to be false - expect(pipeline.status).to eq("skipped") - end - - it "skips builds creation if there is [CI SKIP] tag in commit message" do - commits = [{ message: capMessage }] - pipeline = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - - expect(pipeline).to be_persisted - expect(pipeline.builds.any?).to be false - expect(pipeline.status).to eq("skipped") - end - - it "skips builds creation if there is [SKIP CI] tag in commit message" do - commits = [{ message: capMessageFlip }] - pipeline = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - - expect(pipeline).to be_persisted - expect(pipeline.builds.any?).to be false - expect(pipeline.status).to eq("skipped") - end - - it "does not skips builds creation if there is no [ci skip] or [skip ci] tag in commit message" do - allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" } - - commits = [{ message: "some message" }] - pipeline = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - - expect(pipeline).to be_persisted - expect(pipeline.builds.first.name).to eq("staging") - end - - it "skips builds creation if there is [ci skip] tag in commit message and yaml is invalid" do - stub_ci_pipeline_yaml_file('invalid: file: fiile') - commits = [{ message: message }] - pipeline = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - expect(pipeline).to be_persisted - expect(pipeline.builds.any?).to be false - expect(pipeline.status).to eq("skipped") - expect(pipeline.yaml_errors).to be_nil - end - end - - it "skips build creation if there are already builds" do - allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file) { gitlab_ci_yaml } - - commits = [{ message: "message" }] - pipeline = service.execute(project, user, - ref: 'refs/heads/master', - before: '00000000', - after: '31das312', - commits: commits - ) - expect(pipeline).to be_persisted - expect(pipeline.builds.count(:all)).to eq(2) - - pipeline = service.execute(project, user, - ref: 'refs/heads/master', - before: '00000000', - after: '31das312', - commits: commits - ) - expect(pipeline).to be_persisted - expect(pipeline.builds.count(:all)).to eq(2) - end - - it "creates commit with failed status if yaml is invalid" do - stub_ci_pipeline_yaml_file('invalid: file') - - commits = [{ message: "some message" }] - - pipeline = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - - expect(pipeline).to be_persisted - expect(pipeline.status).to eq("failed") - expect(pipeline.builds.any?).to be false - end - - context 'when there are no jobs for this pipeline' do - before do - config = YAML.dump({ test: { script: 'ls', only: ['feature'] } }) - stub_ci_pipeline_yaml_file(config) - end - - it 'does not create a new pipeline' do - result = service.execute(project, user, - ref: 'refs/heads/master', - before: '00000000', - after: '31das312', - commits: [{ message: 'some msg' }]) - - expect(result).to be_falsey - expect(Ci::Build.all).to be_empty - expect(Ci::Pipeline.count).to eq(0) - end - end - end -end diff --git a/spec/services/delete_user_service_spec.rb b/spec/services/delete_user_service_spec.rb index a65938fa03b4ed6452f798449de831f1908f1068..630458f9efcb4f9210cee05d3ec1bd116bd10a79 100644 --- a/spec/services/delete_user_service_spec.rb +++ b/spec/services/delete_user_service_spec.rb @@ -15,7 +15,7 @@ end it 'will delete the project in the near future' do - expect_any_instance_of(Projects::DestroyService).to receive(:pending_delete!).once + expect_any_instance_of(Projects::DestroyService).to receive(:async_execute).once DeleteUserService.new(current_user).execute(user) end diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..ec26770c3ebc6123c2da955b48b1f6b882ad1a4b --- /dev/null +++ b/spec/services/merge_requests/get_urls_service_spec.rb @@ -0,0 +1,100 @@ +require "spec_helper" + +describe MergeRequests::GetUrlsService do + let(:project) { create(:project, :public) } + let(:service) { MergeRequests::GetUrlsService.new(project) } + let(:source_branch) { "my_branch" } + let(:new_merge_request_url) { "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{source_branch}" } + let(:show_merge_request_url) { "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/#{merge_request.iid}" } + let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" } + let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" } + + describe "#execute" do + shared_examples 'new_merge_request_link' do + it 'returns url to create new merge request' do + result = service.execute(changes) + expect(result).to match([{ + branch_name: source_branch, + url: new_merge_request_url, + new_merge_request: true + }]) + end + end + + shared_examples 'show_merge_request_url' do + it 'returns url to view merge request' do + result = service.execute(changes) + expect(result).to match([{ + branch_name: source_branch, + url: show_merge_request_url, + new_merge_request: false + }]) + end + end + + context 'pushing one completely new branch' do + let(:changes) { new_branch_changes } + it_behaves_like 'new_merge_request_link' + end + + context 'pushing to existing branch but no merge request' do + let(:changes) { existing_branch_changes } + it_behaves_like 'new_merge_request_link' + end + + context 'pushing to existing branch and merge request opened' do + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch) } + let(:changes) { existing_branch_changes } + it_behaves_like 'show_merge_request_url' + end + + context 'pushing to existing branch and merge request is reopened' do + let!(:merge_request) { create(:merge_request, :reopened, source_project: project, source_branch: source_branch) } + let(:changes) { existing_branch_changes } + it_behaves_like 'show_merge_request_url' + end + + context 'pushing to existing branch from forked project' do + let(:user) { create(:user) } + let!(:forked_project) { Projects::ForkService.new(project, user).execute } + let!(:merge_request) { create(:merge_request, source_project: forked_project, target_project: project, source_branch: source_branch) } + let(:changes) { existing_branch_changes } + # Source project is now the forked one + let(:service) { MergeRequests::GetUrlsService.new(forked_project) } + it_behaves_like 'show_merge_request_url' + end + + context 'pushing to existing branch and merge request is closed' do + let!(:merge_request) { create(:merge_request, :closed, source_project: project, source_branch: source_branch) } + let(:changes) { existing_branch_changes } + it_behaves_like 'new_merge_request_link' + end + + context 'pushing to existing branch and merge request is merged' do + let!(:merge_request) { create(:merge_request, :merged, source_project: project, source_branch: source_branch) } + let(:changes) { existing_branch_changes } + it_behaves_like 'new_merge_request_link' + end + + context 'pushing new branch and existing branch (with merge request created) at once' do + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: "existing_branch") } + let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch" } + let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/existing_branch" } + let(:changes) { "#{new_branch_changes}\n#{existing_branch_changes}" } + let(:new_merge_request_url) { "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch" } + + it 'returns 2 urls for both creating new and showing merge request' do + result = service.execute(changes) + expect(result).to match([{ + branch_name: "new_branch", + url: new_merge_request_url, + new_merge_request: true + }, { + branch_name: "existing_branch", + url: show_merge_request_url, + new_merge_request: false + }]) + end + end + end +end diff --git a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb index 8f71d71b0f0f43fc853afdeecd6071c46a0d6617..c4b874682751c550ab3ce83e6d90b25ce9f15046 100644 --- a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb +++ b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb @@ -1,7 +1,6 @@ require 'spec_helper' describe MergeRequests::MergeRequestDiffCacheService do - let(:subject) { MergeRequests::MergeRequestDiffCacheService.new } describe '#execute' do diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb index 4da8146e3d6a41dd59a283bd4d5a952f9d4e939c..520e906b21f357e9b8c909a5d7160f743a819a63 100644 --- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb @@ -110,19 +110,15 @@ context 'properly handles multiple stages' do let(:ref) { mr_merge_if_green_enabled.source_branch } - let(:build) { create(:ci_build, pipeline: pipeline, ref: ref, name: 'build', stage: 'build') } - let(:test) { create(:ci_build, pipeline: pipeline, ref: ref, name: 'test', stage: 'test') } + let!(:build) { create(:ci_build, :created, pipeline: pipeline, ref: ref, name: 'build', stage: 'build') } + let!(:test) { create(:ci_build, :created, pipeline: pipeline, ref: ref, name: 'test', stage: 'test') } + let(:pipeline) { create(:ci_empty_pipeline, ref: mr_merge_if_green_enabled.source_branch, project: project) } before do # This behavior of MergeRequest: we instantiate a new object allow_any_instance_of(MergeRequest).to receive(:pipeline).and_wrap_original do Ci::Pipeline.find(pipeline.id) end - - # We create test after the build - allow(pipeline).to receive(:create_next_builds).and_wrap_original do - test - end end it "doesn't merge if some stages failed" do diff --git a/spec/services/projects/enable_deploy_key_service_spec.rb b/spec/services/projects/enable_deploy_key_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a37510cf159897314d196ad92969370832d81cd1 --- /dev/null +++ b/spec/services/projects/enable_deploy_key_service_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Projects::EnableDeployKeyService, services: true do + let(:deploy_key) { create(:deploy_key, public: true) } + let(:project) { create(:empty_project) } + let(:user) { project.creator} + let!(:params) { { key_id: deploy_key.id } } + + it 'enables the key' do + expect do + service.execute + end.to change { project.deploy_keys.count }.from(0).to(1) + end + + context 'trying to add an unaccessable key' do + let(:another_key) { create(:another_key) } + let!(:params) { { key_id: another_key.id } } + + it 'returns nil if the key cannot be added' do + expect(service.execute).to be nil + end + end + + def service + Projects::EnableDeployKeyService.new(project, user, params) + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0b2fab174a0d3365a60ff3df25f3bb1c497de75c..736a388924251e17c28fda75c0736acba14b5edc 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -49,6 +49,13 @@ License.destroy_all TestLicense.init end + + config.around(:each, :caching) do |example| + caching_store = Rails.cache + Rails.cache = ActiveSupport::Cache::MemoryStore.new if example.metadata[:caching] + example.run + Rails.cache = caching_store + end end FactoryGirl::SyntaxRunner.class_eval do diff --git a/spec/support/api/members_shared_examples.rb b/spec/support/api/members_shared_examples.rb new file mode 100644 index 0000000000000000000000000000000000000000..dab71a35a552c526b6a8b95304ddee1ec5ee2da9 --- /dev/null +++ b/spec/support/api/members_shared_examples.rb @@ -0,0 +1,11 @@ +shared_examples 'a 404 response when source is private' do + before do + source.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE) + end + + it 'returns 404' do + route + + expect(response).to have_http_status(404) + end +end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 16e0ada2c9f9ffd3edf3879b66cd6289076cf3c5..c37344998fa2b35889c934bac1494ac61aab59a7 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -42,7 +42,7 @@ def reenable_backup_sub_tasks before do allow(Dir).to receive(:glob).and_return([]) allow(Dir).to receive(:chdir) - allow(File).to receive(:exists?).and_return(true) + allow(File).to receive(:exist?).and_return(true) allow(Kernel).to receive(:system).and_return(true) allow(FileUtils).to receive(:cp_r).and_return(true) allow(FileUtils).to receive(:mv).and_return(true) diff --git a/spec/teaspoon_env.rb b/spec/teaspoon_env.rb index 1a3bbb9c8cc3fd87bb29a2ccad686a65a5c214dc..5ea020f313c9afeb57f426ed0d0f90655a8d165b 100644 --- a/spec/teaspoon_env.rb +++ b/spec/teaspoon_env.rb @@ -149,7 +149,7 @@ # Specify that you always want a coverage configuration to be used. Otherwise, specify that you want coverage # on the CLI. # Set this to "true" or the name of your coverage config. - # config.use_coverage = nil + config.use_coverage = true # You can have multiple coverage configs by passing a name to config.coverage. # e.g. config.coverage :ci do |coverage| @@ -158,15 +158,15 @@ # Which coverage reports Istanbul should generate. Correlates directly to what Istanbul supports. # # Available: text-summary, text, html, lcov, lcovonly, cobertura, teamcity - # coverage.reports = ["text-summary", "html"] + coverage.reports = ["text-summary", "html"] # The path that the coverage should be written to - when there's an artifact to write to disk. # Note: Relative to `config.root`. - # coverage.output_path = "coverage" + coverage.output_path = "coverage-javascript" # Assets to be ignored when generating coverage reports. Accepts an array of filenames or regular expressions. The # default excludes assets from vendor, gems and support libraries. - # coverage.ignore = [%r{/lib/ruby/gems/}, %r{/vendor/assets/}, %r{/support/}, %r{/(.+)_helper.}] + coverage.ignore = [%r{vendor/}, %r{spec/}] # Various thresholds requirements can be defined, and those thresholds will be checked at the end of a run. If any # aren't met the run will fail with a message. Thresholds can be defined as a percentage (0-100), or nil. diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 607a033c4bdbb96d42e26d7de245ec52081adb8c..391af2bdeab21a6050340f3caf05dcda67555e5b 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -53,7 +53,13 @@ subject { PostReceive.new.perform(pwd(project), key_id, base64_changes) } context "creates a Ci::Pipeline for every change" do - before { stub_ci_pipeline_to_return_yaml_file } + before do + allow_any_instance_of(Ci::CreatePipelineService).to receive(:commit) do + OpenStruct.new(id: '123456') + end + allow_any_instance_of(Ci::CreatePipelineService).to receive(:branch?).and_return(true) + stub_ci_pipeline_to_return_yaml_file + end it { expect{ subject }.to change{ Ci::Pipeline.count }.by(2) } end