diff options
933 files changed, 16953 insertions, 4660 deletions
diff --git a/.codeclimate.yml b/.codeclimate.yml index 9998ddba643..2be8e63e842 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -6,35 +6,35 @@ engines: enabled: true config: languages: - - ruby - - javascript + - ruby + - javascript ratings: paths: - - Gemfile.lock - - "**.erb" - - "**.haml" - - "**.rb" - - "**.rhtml" - - "**.slim" - - "**.inc" - - "**.js" - - "**.jsx" - - "**.module" + - Gemfile.lock + - "**.erb" + - "**.haml" + - "**.rb" + - "**.rhtml" + - "**.slim" + - "**.inc" + - "**.js" + - "**.jsx" + - "**.module" exclude_paths: -- config/ -- db/ -- features/ -- node_modules/ -- spec/ -- vendor/ -- .yarn-cache/ -- tmp/ -- builds/ -- coverage/ -- public/ -- shared/ -- webpack-report/ -- log/ -- backups/ -- coverage-javascript/ -- plugins/ + - config/ + - db/ + - features/ + - node_modules/ + - spec/ + - vendor/ + - .yarn-cache/ + - tmp/ + - builds/ + - coverage/ + - public/ + - shared/ + - webpack-report/ + - log/ + - backups/ + - coverage-javascript/ + - plugins/ diff --git a/.gitignore b/.gitignore index 0696dd217af..627c806787b 100644 --- a/.gitignore +++ b/.gitignore @@ -59,8 +59,6 @@ eslint-report.html /public/uploads.* /public/uploads/ /shared/artifacts/ -/spec/javascripts/fixtures/blob/pdf/ -/spec/javascripts/fixtures/blob/balsamiq/ /rails_best_practices_output.html /tags /tmp/* diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 44beccd966a..c971df3ba5f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-golang-1.11-git-2.21-chrome-73.0-node-10.x-yarn-1.12-postgresql-9.6-graphicsmagick-1.3.29" +image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.21-chrome-73.0-node-10.x-yarn-1.12-postgresql-9.6-graphicsmagick-1.3.29" variables: MYSQL_ALLOW_EMPTY_PASSWORD: "1" diff --git a/.gitlab/ci/cng.gitlab-ci.yml b/.gitlab/ci/cng.gitlab-ci.yml index c384bcdcdfc..d624e8d09f6 100644 --- a/.gitlab/ci/cng.gitlab-ci.yml +++ b/.gitlab/ci/cng.gitlab-ci.yml @@ -1,5 +1,5 @@ cloud-native-image: - image: ruby:2.5-alpine + image: ruby:2.6-alpine before_script: [] dependencies: [] stage: post-test diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml index fbf8925e30a..986ba7558d5 100644 --- a/.gitlab/ci/frontend.gitlab-ci.yml +++ b/.gitlab/ci/frontend.gitlab-ci.yml @@ -16,7 +16,7 @@ gitlab:assets:compile: <<: *assets-compile-cache extends: .dedicated-no-docs-pull-cache-job - image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-git-2.21-chrome-73.0-node-8.x-yarn-1.12-graphicsmagick-1.3.29-docker-18.06.1 + image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-git-2.21-chrome-73.0-node-8.x-yarn-1.12-graphicsmagick-1.3.29-docker-18.06.1 dependencies: - setup-test-env services: diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml index 466c47b37c7..cf87f5eb39c 100644 --- a/.gitlab/ci/global.gitlab-ci.yml +++ b/.gitlab/ci/global.gitlab-ci.yml @@ -9,7 +9,7 @@ - gitlab-org .default-cache: &default-cache - key: "debian-stretch-ruby-2.5.3-node-10.x" + key: "debian-stretch-ruby-2.6.3-node-10.x" paths: - vendor/ruby - .yarn-cache/ @@ -47,7 +47,7 @@ .single-script-job-dedicated-runner: extends: .dedicated-runner - image: ruby:2.5-alpine + image: ruby:2.6-alpine stage: test cache: {} dependencies: [] diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml index 85c6409186e..122ed622ee2 100644 --- a/.gitlab/ci/qa.gitlab-ci.yml +++ b/.gitlab/ci/qa.gitlab-ci.yml @@ -1,5 +1,5 @@ package-and-qa: - image: ruby:2.5-alpine + image: ruby:2.6-alpine stage: qa when: manual before_script: [] diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml index 35c5f67427e..29534e40a14 100644 --- a/.gitlab/ci/rails.gitlab-ci.yml +++ b/.gitlab/ci/rails.gitlab-ci.yml @@ -86,7 +86,7 @@ .rspec-metadata-pg-10: &rspec-metadata-pg-10 <<: *rspec-metadata <<: *use-pg-10 - image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-golang-1.11-git-2.21-chrome-73.0-node-10.x-yarn-1.12-postgresql-10-graphicsmagick-1.3.29" + image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.21-chrome-73.0-node-10.x-yarn-1.12-postgresql-10-graphicsmagick-1.3.29" .rspec-metadata-mysql: &rspec-metadata-mysql <<: *rspec-metadata @@ -108,7 +108,8 @@ - git fetch https://gitlab.com/gitlab-org/gitlab-ce.git v9.3.0 - git checkout -f FETCH_HEAD - sed -i "s/gem 'oj', '~> 2.17.4'//" Gemfile - - bundle update google-protobuf grpc + - sed -i "s/gem 'bootsnap', '~> 1.0.0'/gem 'bootsnap'/" Gemfile + - bundle update google-protobuf grpc bootsnap - bundle install $BUNDLE_INSTALL_FLAGS - date - cp config/gitlab.yml.example config/gitlab.yml @@ -183,7 +184,7 @@ static-analysis: script: - scripts/static-analysis cache: - key: "debian-stretch-ruby-2.5.3-node-10.x-and-rubocop" + key: "debian-stretch-ruby-2.6.3-node-10.x-and-rubocop" paths: - vendor/ruby - .yarn-cache/ diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml index ae16549ef6b..80356fa1dc2 100644 --- a/.gitlab/ci/review.gitlab-ci.yml +++ b/.gitlab/ci/review.gitlab-ci.yml @@ -54,7 +54,7 @@ build-qa-image: - time docker push ${QA_IMAGE} .review-build-cng-base: &review-build-cng-base - image: ruby:2.5-alpine + image: ruby:2.6-alpine stage: test when: manual before_script: @@ -78,7 +78,6 @@ schedule:review-build-cng: <<: *review-base stage: review retry: 2 - allow_failure: true variables: HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}" DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}" @@ -130,7 +129,6 @@ review-stop: .review-qa-base: &review-qa-base <<: *review-docker stage: qa - allow_failure: true variables: <<: *review-docker-variables QA_ARTIFACTS_DIR: "${CI_PROJECT_DIR}/qa" @@ -165,13 +163,14 @@ review-qa-smoke: review-qa-all: <<: *review-qa-base + allow_failure: true when: manual script: - gitlab-qa Test::Instance::Any "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}" .review-performance-base: &review-performance-base <<: *review-qa-base - stage: qa + allow_failure: true before_script: - export CI_ENVIRONMENT_URL="$(cat review_app_url.txt)" - echo "${CI_ENVIRONMENT_URL}" diff --git a/.gitlab/ci/test-metadata.gitlab-ci.yml b/.gitlab/ci/test-metadata.gitlab-ci.yml index 3ba7af956b5..4b595083ec6 100644 --- a/.gitlab/ci/test-metadata.gitlab-ci.yml +++ b/.gitlab/ci/test-metadata.gitlab-ci.yml @@ -56,7 +56,7 @@ update-tests-metadata: flaky-examples-check: extends: .dedicated-runner - image: ruby:2.5-alpine + image: ruby:2.6-alpine services: [] before_script: [] variables: diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md index 3e58d2a867e..3adea22b33a 100644 --- a/.gitlab/issue_templates/Bug.md +++ b/.gitlab/issue_templates/Bug.md @@ -27,9 +27,9 @@ and verify the issue you're about to submit isn't a duplicate. ### Example Project -(If possible, please create an example project here on GitLab.com that exhibits the problematic behaviour, and link to it here in the bug report) +(If possible, please create an example project here on GitLab.com that exhibits the problematic behavior, and link to it here in the bug report) -(If you are using an older version of GitLab, this will also determine whether the bug has been fixed in a more recent version) +(If you are using an older version of GitLab, this will also determine whether the bug is fixed in a more recent version) ### What is the current *bug* behavior? @@ -42,7 +42,7 @@ and verify the issue you're about to submit isn't a duplicate. ### Relevant logs and/or screenshots (Paste any relevant logs - please use code blocks (```) to format console output, -logs, and code as it's very hard to read otherwise.) +logs, and code as it's tough to read otherwise.) ### Output of checks diff --git a/.gitlab/issue_templates/Security developer workflow.md b/.gitlab/issue_templates/Security developer workflow.md index 9946651075f..7857afb66c2 100644 --- a/.gitlab/issue_templates/Security developer workflow.md +++ b/.gitlab/issue_templates/Security developer workflow.md @@ -17,10 +17,10 @@ Set the title to: `Description of the original issue` #### Backports -- [ ] Once the MR is ready to be merged, create MRs targetting the last 3 releases, plus the current RC if between the 7th and 22nd of the month. +- [ ] Once the MR is ready to be merged, create MRs targeting the last 3 releases, plus the current RC if between the 7th and 22nd of the month. - [ ] At this point, it might be easy to squash the commits from the MR into one - You can use the script `bin/secpick` instead of the following steps, to help you cherry-picking. See the [secpick documentation] - - [ ] Create each MR targetting the stable branch `X-Y-stable`, using the "Security Release" merge request template. + - [ ] Create each MR targeting the stable branch `X-Y-stable`, using the "Security Release" merge request template. - Every merge request will have its own set of TODOs, so make sure to complete those. - [ ] Make sure all MRs have a link in the [links section](#links) diff --git a/.haml-lint.yml b/.haml-lint.yml index e9cc4a91a21..e356be30c45 100644 --- a/.haml-lint.yml +++ b/.haml-lint.yml @@ -153,7 +153,7 @@ linters: Indentation: enabled: true - character: space # or tab + character: space # or tab TagName: enabled: true diff --git a/.pkgr.yml b/.pkgr.yml index 10bcd7bd4bd..2e741f41a9e 100644 --- a/.pkgr.yml +++ b/.pkgr.yml @@ -3,8 +3,8 @@ group: git services: - postgres before_precompile: ./bin/pkgr_before_precompile.sh -env: - - SKIP_STORAGE_VALIDATION=true +env: + - SKIP_STORAGE_VALIDATION=true targets: debian-7: &wheezy build_dependencies: diff --git a/.ruby-version b/.ruby-version index aedc15bb0c6..ec1cf33c3f6 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.5.3 +2.6.3 diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 7d47e599800..e640847f99c 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1.41.0 +1.42.1 diff --git a/GITLAB_ELASTICSEARCH_INDEXER_VERSION b/GITLAB_ELASTICSEARCH_INDEXER_VERSION new file mode 100644 index 00000000000..9084fa2f716 --- /dev/null +++ b/GITLAB_ELASTICSEARCH_INDEXER_VERSION @@ -0,0 +1 @@ +1.1.0 diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index f7ee06693c1..deeb3d66ef0 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -9.0.0 +9.2.0 @@ -43,6 +43,7 @@ gem 'omniauth_crowd', '~> 2.2.0' gem 'omniauth-authentiq', '~> 0.3.3' gem 'omniauth_openid_connect', '~> 0.3.0' gem "omniauth-ultraauth", '~> 0.0.2' +gem 'omniauth-salesforce', '~> 1.0.5' gem 'rack-oauth2', '~> 1.9.3' gem 'jwt', '~> 2.1.0' @@ -59,6 +60,8 @@ gem 'u2f', '~> 0.2.1' # GitLab Pages gem 'validates_hostname', '~> 1.0.6' gem 'rubyzip', '~> 1.2.2', require: 'zip' +# GitLab Pages letsencrypt support +gem 'acme-client', '~> 2.0.2' # Browser detection gem 'browser', '~> 2.5' @@ -166,7 +169,7 @@ gem 'redis-namespace', '~> 1.6.0' gem 'gitlab-sidekiq-fetcher', '~> 0.4.0', require: 'sidekiq-reliable-fetch' # Cron Parser -gem 'fugit', '~> 1.1' +gem 'fugit', '~> 1.2.1' # HTTP requests gem 'httparty', '~> 0.16.4' @@ -350,14 +353,14 @@ group :development, :test do gem 'spring', '~> 2.0.0' gem 'spring-commands-rspec', '~> 1.0.4' - gem 'gitlab-styles', '~> 2.6', require: false + gem 'gitlab-styles', '~> 2.7', require: false # Pin these dependencies, otherwise a new rule could break the CI pipelines - gem 'rubocop', '~> 0.68.1' + gem 'rubocop', '~> 0.69.0' gem 'rubocop-performance', '~> 1.1.0' gem 'rubocop-rspec', '~> 1.22.1' gem 'scss_lint', '~> 0.56.0', require: false - gem 'haml_lint', '~> 0.30.0', require: false + gem 'haml_lint', '~> 0.31.0', require: false gem 'simplecov', '~> 0.14.0', require: false gem 'bundler-audit', '~> 0.5.0', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 9b1a036030a..be722b89a40 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,6 +4,8 @@ GEM RedCloth (4.3.2) abstract_type (0.0.7) ace-rails-ap (4.1.2) + acme-client (2.0.2) + faraday (~> 0.9, >= 0.9.1) actioncable (5.1.7) actionpack (= 5.1.7) nio4r (~> 2.0) @@ -85,7 +87,7 @@ GEM erubi (>= 1.0.0) rack (>= 0.9.0) bindata (2.4.3) - binding_ninja (0.2.2) + binding_ninja (0.2.3) binding_of_caller (0.8.0) debug_inspector (>= 0.0.1) bootsnap (1.4.1) @@ -190,7 +192,7 @@ GEM equalizer (0.0.11) erubi (1.8.0) escape_utils (1.2.1) - et-orbi (1.1.7) + et-orbi (1.2.1) tzinfo eventmachine (1.2.7) excon (0.62.0) @@ -264,15 +266,15 @@ GEM foreman (0.84.0) thor (~> 0.19.1) formatador (0.2.5) - fugit (1.1.9) - et-orbi (~> 1.1, >= 1.1.7) + fugit (1.2.1) + et-orbi (~> 1.1, >= 1.1.8) raabro (~> 1.1) fuubar (2.2.0) rspec-core (~> 3.0) ruby-progressbar (~> 1.4) gemojione (3.3.0) json - get_process_mem (0.2.0) + get_process_mem (0.2.3) gettext (3.2.9) locale (>= 2.0.5) text (>= 1.3.0) @@ -297,8 +299,8 @@ GEM gitlab-markup (1.7.0) gitlab-sidekiq-fetcher (0.4.0) sidekiq (~> 5) - gitlab-styles (2.6.2) - rubocop (~> 0.68.1) + gitlab-styles (2.7.0) + rubocop (~> 0.69.0) rubocop-gitlab-security (~> 0.1.0) rubocop-performance (~> 1.1.0) rubocop-rspec (~> 1.19) @@ -358,7 +360,7 @@ GEM haml (5.0.4) temple (>= 0.8.0) tilt - haml_lint (0.30.0) + haml_lint (0.31.0) haml (>= 4.0, < 5.1) rainbow rake (>= 10, < 13) @@ -553,6 +555,9 @@ GEM omniauth (~> 1.9) omniauth-oauth2-generic (0.2.2) omniauth-oauth2 (~> 1.0) + omniauth-salesforce (1.0.5) + omniauth (~> 1.0) + omniauth-oauth2 (~> 1.0) omniauth-saml (1.10.0) omniauth (~> 1.3, >= 1.3.2) ruby-saml (~> 1.7) @@ -588,7 +593,7 @@ GEM orm_adapter (0.5.0) os (1.0.0) parallel (1.17.0) - parser (2.5.3.0) + parser (2.6.3.0) ast (~> 2.4.0) parslet (1.8.2) peek (1.0.1) @@ -763,8 +768,8 @@ GEM rspec-mocks (3.7.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.7.0) - rspec-parameterized (0.4.1) - binding_ninja (>= 0.2.1) + rspec-parameterized (0.4.2) + binding_ninja (>= 0.2.3) parser proc_to_ast rspec (>= 2.13, < 4) @@ -788,13 +793,13 @@ GEM pg rails sqlite3 - rubocop (0.68.1) + rubocop (0.69.0) jaro_winkler (~> 1.5.1) parallel (~> 1.10) - parser (>= 2.5, != 2.5.1.1) + parser (>= 2.6) rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 1.6) + unicode-display_width (>= 1.4.0, < 1.7) rubocop-gitlab-security (0.1.1) rubocop (>= 0.51) rubocop-performance (1.1.0) @@ -809,7 +814,7 @@ GEM ruby-progressbar (1.10.0) ruby-saml (1.7.2) nokogiri (>= 1.5.10) - ruby_parser (3.11.0) + ruby_parser (3.13.1) sexp_processor (~> 4.9) rubyntlm (0.6.2) rubypants (0.2.0) @@ -849,7 +854,7 @@ GEM sentry-raven (2.9.0) faraday (>= 0.7.6, < 1.0) settingslogic (2.0.9) - sexp_processor (4.11.0) + sexp_processor (4.12.0) sham_rack (1.3.6) rack shoulda-matchers (3.1.2) @@ -932,7 +937,7 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.7.5) - unicode-display_width (1.5.0) + unicode-display_width (1.6.0) unicorn (5.4.1) kgio (~> 2.6) raindrops (~> 0.7) @@ -940,13 +945,13 @@ GEM get_process_mem (~> 0) unicorn (>= 4, < 6) uniform_notifier (1.10.0) - unparser (0.4.2) + unparser (0.4.5) abstract_type (~> 0.0.7) adamantium (~> 0.2.0) concord (~> 0.1.5) diff-lcs (~> 1.3) equalizer (~> 0.0.9) - parser (>= 2.3.1.2, < 2.6) + parser (~> 2.6.3) procto (~> 0.0.2) validate_email (0.1.6) activemodel (>= 3.0) @@ -993,6 +998,7 @@ PLATFORMS DEPENDENCIES RedCloth (~> 4.3.2) ace-rails-ap (~> 4.1.0) + acme-client (~> 2.0.2) activerecord_sane_schema_dumper (= 1.0) acts-as-taggable-on (~> 6.0) addressable (~> 2.5.2) @@ -1056,7 +1062,7 @@ DEPENDENCIES fog-rackspace (~> 0.1.1) font-awesome-rails (~> 4.7) foreman (~> 0.84.0) - fugit (~> 1.1) + fugit (~> 1.2.1) fuubar (~> 2.2.0) gemojione (~> 3.3) gettext (~> 3.2.2) @@ -1068,7 +1074,7 @@ DEPENDENCIES gitlab-labkit (~> 0.2.0) gitlab-markup (~> 1.7.0) gitlab-sidekiq-fetcher (~> 0.4.0) - gitlab-styles (~> 2.6) + gitlab-styles (~> 2.7) gitlab_omniauth-ldap (~> 2.1.1) gon (~> 6.2) google-api-client (~> 0.23) @@ -1081,7 +1087,7 @@ DEPENDENCIES graphiql-rails (~> 1.4.10) graphql (~> 1.8.0) grpc (~> 1.19.0) - haml_lint (~> 0.30.0) + haml_lint (~> 0.31.0) hamlit (~> 2.8.8) hangouts-chat (~> 0.0.5) hashie-forbidden_attributes @@ -1127,6 +1133,7 @@ DEPENDENCIES omniauth-google-oauth2 (~> 0.6.0) omniauth-kerberos (~> 0.3.0) omniauth-oauth2-generic (~> 0.2.2) + omniauth-salesforce (~> 1.0.5) omniauth-saml (~> 1.10) omniauth-shibboleth (~> 1.3.0) omniauth-twitter (~> 1.4) @@ -1175,7 +1182,7 @@ DEPENDENCIES rspec-set (~> 0.1.3) rspec_junit_formatter rspec_profiling (~> 0.0.5) - rubocop (~> 0.68.1) + rubocop (~> 0.69.0) rubocop-performance (~> 1.1.0) rubocop-rspec (~> 1.22.1) ruby-fogbugz (~> 0.2.1) diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js index 798114b4b0b..d0b7f3ff7a2 100644 --- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js +++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js @@ -15,7 +15,7 @@ import { sprintf, __ } from '../../locale'; // </pre> // -// This is an arbitary number; Can be iterated upon when suitable. +// This is an arbitrary number; Can be iterated upon when suitable. const MAX_CHAR_LIMIT = 5000; export default function renderMermaid($els) { diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js new file mode 100644 index 00000000000..da82b52330a --- /dev/null +++ b/app/assets/javascripts/boards/stores/actions.js @@ -0,0 +1,65 @@ +const notImplemented = () => { + throw new Error('Not implemented!'); +}; + +export default { + setEndpoints: () => { + notImplemented(); + }, + + fetchLists: () => { + notImplemented(); + }, + + generateDefaultLists: () => { + notImplemented(); + }, + + createList: () => { + notImplemented(); + }, + + updateList: () => { + notImplemented(); + }, + + deleteList: () => { + notImplemented(); + }, + + fetchIssuesForList: () => { + notImplemented(); + }, + + moveIssue: () => { + notImplemented(); + }, + + createNewIssue: () => { + notImplemented(); + }, + + fetchBacklog: () => { + notImplemented(); + }, + + bulkUpdateIssues: () => { + notImplemented(); + }, + + fetchIssue: () => { + notImplemented(); + }, + + toggleIssueSubscription: () => { + notImplemented(); + }, + + showPage: () => { + notImplemented(); + }, + + toggleEmptyState: () => { + notImplemented(); + }, +}; diff --git a/app/assets/javascripts/boards/stores/index.js b/app/assets/javascripts/boards/stores/index.js new file mode 100644 index 00000000000..f70395a3771 --- /dev/null +++ b/app/assets/javascripts/boards/stores/index.js @@ -0,0 +1,14 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import state from 'ee_else_ce/boards/stores/state'; +import actions from 'ee_else_ce/boards/stores/actions'; +import mutations from 'ee_else_ce/boards/stores/mutations'; + +Vue.use(Vuex); + +export default () => + new Vuex.Store({ + state, + actions, + mutations, + }); diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js new file mode 100644 index 00000000000..fcdfa6799b6 --- /dev/null +++ b/app/assets/javascripts/boards/stores/mutation_types.js @@ -0,0 +1,21 @@ +export const SET_ENDPOINTS = 'SET_ENDPOINTS'; +export const REQUEST_ADD_LIST = 'REQUEST_ADD_LIST'; +export const RECEIVE_ADD_LIST_SUCCESS = 'RECEIVE_ADD_LIST_SUCCESS'; +export const RECEIVE_ADD_LIST_ERROR = 'RECEIVE_ADD_LIST_ERROR'; +export const REQUEST_UPDATE_LIST = 'REQUEST_UPDATE_LIST'; +export const RECEIVE_UPDATE_LIST_SUCCESS = 'RECEIVE_UPDATE_LIST_SUCCESS'; +export const RECEIVE_UPDATE_LIST_ERROR = 'RECEIVE_UPDATE_LIST_ERROR'; +export const REQUEST_REMOVE_LIST = 'REQUEST_REMOVE_LIST'; +export const RECEIVE_REMOVE_LIST_SUCCESS = 'RECEIVE_REMOVE_LIST_SUCCESS'; +export const RECEIVE_REMOVE_LIST_ERROR = 'RECEIVE_REMOVE_LIST_ERROR'; +export const REQUEST_ADD_ISSUE = 'REQUEST_ADD_ISSUE'; +export const RECEIVE_ADD_ISSUE_SUCCESS = 'RECEIVE_ADD_ISSUE_SUCCESS'; +export const RECEIVE_ADD_ISSUE_ERROR = 'RECEIVE_ADD_ISSUE_ERROR'; +export const REQUEST_MOVE_ISSUE = 'REQUEST_MOVE_ISSUE'; +export const RECEIVE_MOVE_ISSUE_SUCCESS = 'RECEIVE_MOVE_ISSUE_SUCCESS'; +export const RECEIVE_MOVE_ISSUE_ERROR = 'RECEIVE_MOVE_ISSUE_ERROR'; +export const REQUEST_UPDATE_ISSUE = 'REQUEST_UPDATE_ISSUE'; +export const RECEIVE_UPDATE_ISSUE_SUCCESS = 'RECEIVE_UPDATE_ISSUE_SUCCESS'; +export const RECEIVE_UPDATE_ISSUE_ERROR = 'RECEIVE_UPDATE_ISSUE_ERROR'; +export const SET_CURRENT_PAGE = 'SET_CURRENT_PAGE'; +export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE'; diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js new file mode 100644 index 00000000000..77ba68be07e --- /dev/null +++ b/app/assets/javascripts/boards/stores/mutations.js @@ -0,0 +1,91 @@ +import * as mutationTypes from './mutation_types'; + +const notImplemented = () => { + throw new Error('Not implemented!'); +}; + +export default { + [mutationTypes.SET_ENDPOINTS]: () => { + notImplemented(); + }, + + [mutationTypes.REQUEST_ADD_LIST]: () => { + notImplemented(); + }, + + [mutationTypes.RECEIVE_ADD_LIST_SUCCESS]: () => { + notImplemented(); + }, + + [mutationTypes.RECEIVE_ADD_LIST_ERROR]: () => { + notImplemented(); + }, + + [mutationTypes.REQUEST_UPDATE_LIST]: () => { + notImplemented(); + }, + + [mutationTypes.RECEIVE_UPDATE_LIST_SUCCESS]: () => { + notImplemented(); + }, + + [mutationTypes.RECEIVE_UPDATE_LIST_ERROR]: () => { + notImplemented(); + }, + + [mutationTypes.REQUEST_REMOVE_LIST]: () => { + notImplemented(); + }, + + [mutationTypes.RECEIVE_REMOVE_LIST_SUCCESS]: () => { + notImplemented(); + }, + + [mutationTypes.RECEIVE_REMOVE_LIST_ERROR]: () => { + notImplemented(); + }, + + [mutationTypes.REQUEST_ADD_ISSUE]: () => { + notImplemented(); + }, + + [mutationTypes.RECEIVE_ADD_ISSUE_SUCCESS]: () => { + notImplemented(); + }, + + [mutationTypes.RECEIVE_ADD_ISSUE_ERROR]: () => { + notImplemented(); + }, + + [mutationTypes.REQUEST_MOVE_ISSUE]: () => { + notImplemented(); + }, + + [mutationTypes.RECEIVE_MOVE_ISSUE_SUCCESS]: () => { + notImplemented(); + }, + + [mutationTypes.RECEIVE_MOVE_ISSUE_ERROR]: () => { + notImplemented(); + }, + + [mutationTypes.REQUEST_UPDATE_ISSUE]: () => { + notImplemented(); + }, + + [mutationTypes.RECEIVE_UPDATE_ISSUE_SUCCESS]: () => { + notImplemented(); + }, + + [mutationTypes.RECEIVE_UPDATE_ISSUE_ERROR]: () => { + notImplemented(); + }, + + [mutationTypes.SET_CURRENT_PAGE]: () => { + notImplemented(); + }, + + [mutationTypes.TOGGLE_EMPTY_STATE]: () => { + notImplemented(); + }, +}; diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js new file mode 100644 index 00000000000..dd16abb01a5 --- /dev/null +++ b/app/assets/javascripts/boards/stores/state.js @@ -0,0 +1,3 @@ +export default () => ({ + // ... +}); diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js index 03dea1ec0a5..b62ec8a651b 100644 --- a/app/assets/javascripts/contextual_sidebar.js +++ b/app/assets/javascripts/contextual_sidebar.js @@ -8,6 +8,8 @@ import { parseBoolean } from '~/lib/utils/common_utils'; // https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24555#note_134136110 const NAV_SIDEBAR_BREAKPOINT = 1200; +export const SIDEBAR_COLLAPSED_CLASS = 'js-sidebar-collapsed'; + export default class ContextualSidebar { constructor() { this.initDomElements(); @@ -62,6 +64,7 @@ export default class ContextualSidebar { const breakpoint = bp.getBreakpointSize(); const dbp = ContextualSidebar.isDesktopBreakpoint(); + this.$sidebar.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show); this.$sidebar.toggleClass('sidebar-expanded-mobile', !dbp ? show : false); this.$overlay.toggleClass( 'mobile-nav-open', diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 0ed4dcdcd81..11d6672cacf 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -157,10 +157,12 @@ export default { this.adjustView(); eventHub.$once('fetchedNotesData', this.setDiscussions); eventHub.$once('fetchDiffData', this.fetchData); + eventHub.$on('refetchDiffData', this.refetchDiffData); this.CENTERED_LIMITED_CONTAINER_CLASSES = CENTERED_LIMITED_CONTAINER_CLASSES; }, beforeDestroy() { eventHub.$off('fetchDiffData', this.fetchData); + eventHub.$off('refetchDiffData', this.refetchDiffData); this.removeEventListeners(); }, methods: { @@ -175,10 +177,16 @@ export default { 'scrollToFile', 'toggleShowTreeList', ]), - fetchData() { + refetchDiffData() { + this.assignedDiscussions = false; + this.fetchData(false); + }, + fetchData(toggleTree = true) { this.fetchDiffFiles() .then(() => { - this.hideTreeListIfJustOneFile(); + if (toggleTree) { + this.hideTreeListIfJustOneFile(); + } requestIdleCallback( () => { diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue index 384f33e0983..30be2e68e76 100644 --- a/app/assets/javascripts/diffs/components/tree_list.vue +++ b/app/assets/javascripts/diffs/components/tree_list.vue @@ -1,6 +1,7 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; import { GlTooltipDirective } from '@gitlab/ui'; +import { s__, sprintf } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; import FileRow from '~/vue_shared/components/file_row.vue'; import FileRowStats from './file_row_stats.vue'; @@ -57,6 +58,9 @@ export default { this.search = ''; }, }, + searchPlaceholder: sprintf(s__('MergeRequest|Filter files or search with %{modifier_key}+p'), { + modifier_key: /Mac/i.test(navigator.userAgent) ? 'cmd' : 'ctrl', + }), }; </script> @@ -65,10 +69,13 @@ export default { <div class="append-bottom-8 position-relative tree-list-search d-flex"> <div class="flex-fill d-flex"> <icon name="search" class="position-absolute tree-list-icon" /> + <label for="diff-tree-search" class="sr-only">{{ $options.searchPlaceholder }}</label> <input + id="diff-tree-search" v-model="search" - :placeholder="s__('MergeRequest|Filter files')" + :placeholder="$options.searchPlaceholder" type="search" + name="diff-tree-search" class="form-control" /> <button diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 386d08aed2b..35297b7c264 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -52,7 +52,7 @@ export const fetchDiffFiles = ({ state, commit }) => { }); return axios - .get(state.endpoint, { params: { w: state.showWhitespace ? null : '1' } }) + .get(mergeUrlParams({ w: state.showWhitespace ? '0' : '1' }, state.endpoint)) .then(res => { commit(types.SET_LOADING, false); commit(types.SET_MERGE_REQUEST_DIFFS, res.data.merge_request_diffs || []); @@ -125,7 +125,8 @@ export const startRenderDiffsQueue = ({ state, commit }) => { new Promise(resolve => { const nextFile = state.diffFiles.find( file => - !file.renderIt && (!file.viewer.collapsed || !file.viewer.name === diffViewerModes.text), + !file.renderIt && + (file.viewer && (!file.viewer.collapsed || !file.viewer.name === diffViewerModes.text)), ); if (nextFile) { @@ -315,8 +316,10 @@ export const setShowWhitespace = ({ commit }, { showWhitespace, pushState = fals localStorage.setItem(WHITESPACE_STORAGE_KEY, showWhitespace); if (pushState) { - historyPushState(showWhitespace ? '?w=0' : '?w=1'); + historyPushState(mergeUrlParams({ w: showWhitespace ? '0' : '1' }, window.location.href)); } + + eventHub.$emit('refetchDiffData'); }; export const toggleFileFinder = ({ commit }, visible) => { diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js index 2b6af9060d1..2566ed6b47c 100644 --- a/app/assets/javascripts/fly_out_nav.js +++ b/app/assets/javascripts/fly_out_nav.js @@ -1,4 +1,5 @@ import bp from './breakpoints'; +import { SIDEBAR_COLLAPSED_CLASS } from './contextual_sidebar'; const HIDE_INTERVAL_TIMEOUT = 300; const IS_OVER_CLASS = 'is-over'; @@ -29,7 +30,7 @@ const setHeaderHeight = () => { }; export const isSidebarCollapsed = () => - sidebar && sidebar.classList.contains('sidebar-collapsed-desktop'); + sidebar && sidebar.classList.contains(SIDEBAR_COLLAPSED_CLASS); export const canShowActiveSubItems = el => { if (el.classList.contains('active') && !isSidebarCollapsed()) { diff --git a/app/assets/javascripts/gl_field_error.js b/app/assets/javascripts/gl_field_error.js index a5b8c357e8a..04301c9ce12 100644 --- a/app/assets/javascripts/gl_field_error.js +++ b/app/assets/javascripts/gl_field_error.js @@ -1,4 +1,5 @@ import $ from 'jquery'; +import { __ } from '~/locale'; /** * This class overrides the browser's validation error bubbles, displaying custom @@ -61,7 +62,7 @@ export default class GlFieldError { this.inputElement = $(input); this.inputDomElement = this.inputElement.get(0); this.form = formErrors; - this.errorMessage = this.inputElement.attr('title') || 'This field is required.'; + this.errorMessage = this.inputElement.attr('title') || __('This field is required.'); this.fieldErrorElement = $(`<p class='${errorMessageClass} hidden'>${this.errorMessage}</p>`); this.state = { diff --git a/app/assets/javascripts/gpg_badges.js b/app/assets/javascripts/gpg_badges.js index efba6fc1aff..96051b612b5 100644 --- a/app/assets/javascripts/gpg_badges.js +++ b/app/assets/javascripts/gpg_badges.js @@ -20,7 +20,7 @@ export default class GpgBadges { const endpoint = tag.data('signaturesPath'); if (!endpoint) { displayError(); - return Promise.reject(new Error('Missing commit signatures endpoint!')); + return Promise.reject(new Error(__('Missing commit signatures endpoint!'))); } const params = parseQueryStringIntoObject(tag.serialize()); diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index bdadbb1bb2a..a1263d1cdab 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import axios from './lib/utils/axios_utils'; import Api from './api'; import { normalizeHeaders } from './lib/utils/common_utils'; +import { __ } from '~/locale'; export default function groupsSelect() { import(/* webpackChunkName: 'select2' */ 'select2/select2') @@ -18,7 +19,7 @@ export default function groupsSelect() { : Api.groupsPath; $select.select2({ - placeholder: 'Search for a group', + placeholder: __('Search for a group'), allowClear: $select.hasClass('allowClear'), multiple: $select.hasClass('multiselect'), minimumInputLength: 0, diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js index ccbe591a63e..bc9d7fcf30d 100644 --- a/app/assets/javascripts/issuable_bulk_update_actions.js +++ b/app/assets/javascripts/issuable_bulk_update_actions.js @@ -4,6 +4,7 @@ import $ from 'jquery'; import _ from 'underscore'; import axios from './lib/utils/axios_utils'; import Flash from './flash'; +import { __ } from './locale'; export default { init({ container, form, issues, prefixId } = {}) { @@ -32,7 +33,7 @@ export default { onFormSubmitFailure() { this.form.find('[type="submit"]').enable(); - return new Flash('Issue update failed'); + return new Flash(__('Issue update failed')); }, getSelectedIssues() { diff --git a/app/assets/javascripts/issuable_index.js b/app/assets/javascripts/issuable_index.js index ffcbd7cf28c..f51c7a2f990 100644 --- a/app/assets/javascripts/issuable_index.js +++ b/app/assets/javascripts/issuable_index.js @@ -1,7 +1,7 @@ import $ from 'jquery'; import axios from './lib/utils/axios_utils'; import flash from './flash'; -import { __ } from './locale'; +import { s__, __ } from './locale'; import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; @@ -29,7 +29,7 @@ export default class IssuableIndex { $resetToken.on('click', e => { e.preventDefault(); - $resetToken.text('resetting...'); + $resetToken.text(s__('EmailToken|resetting...')); axios .put($resetToken.attr('href')) @@ -38,12 +38,12 @@ export default class IssuableIndex { .val(data.new_address) .focus(); - $resetToken.text('reset it'); + $resetToken.text(s__('EmailToken|reset it')); }) .catch(() => { flash(__('There was an error when reseting email token.')); - $resetToken.text('reset it'); + $resetToken.text(s__('EmailToken|reset it')); }); }); } diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index cd1afb6ba83..db4607ca58d 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -7,6 +7,7 @@ import flash from './flash'; import TaskList from './task_list'; import CreateMergeRequestDropdown from './create_merge_request_dropdown'; import IssuablesHelper from './helpers/issuables_helper'; +import { __ } from './locale'; export default class Issue { constructor() { @@ -44,7 +45,11 @@ export default class Issue { * @param {Array} data * @param {String} issueFailMessage */ - updateTopState(isClosed, data, issueFailMessage = 'Unable to update this issue at this time.') { + updateTopState( + isClosed, + data, + issueFailMessage = __('Unable to update this issue at this time.'), + ) { if ('id' in data) { const isClosedBadge = $('div.status-box-issue-closed'); const isOpenBadge = $('div.status-box-open'); @@ -81,7 +86,7 @@ export default class Issue { } initIssueBtnEventListeners() { - const issueFailMessage = 'Unable to update this issue at this time.'; + const issueFailMessage = __('Unable to update this issue at this time.'); return $(document).on( 'click', @@ -152,6 +157,6 @@ export default class Issue { $container.html(data.html); } }) - .catch(() => flash('Failed to load related branches')); + .catch(() => flash(__('Failed to load related branches'))); } } diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js index c14803c80e7..75edff41a89 100644 --- a/app/assets/javascripts/issue_status_select.js +++ b/app/assets/javascripts/issue_status_select.js @@ -1,4 +1,5 @@ import $ from 'jquery'; +import { __ } from './locale'; export default function issueStatusSelect() { $('.js-issue-status').each((i, el) => { @@ -7,7 +8,7 @@ export default function issueStatusSelect() { selectable: true, fieldName, toggleLabel(selected, element, instance) { - let label = 'Author'; + let label = __('Author'); const $item = instance.dropdown.find('.is-active'); if ($item.length) { label = $item.text(); diff --git a/app/assets/javascripts/lib/utils/highlight.js b/app/assets/javascripts/lib/utils/highlight.js index 4f7eff2cca1..8f0afa3467d 100644 --- a/app/assets/javascripts/lib/utils/highlight.js +++ b/app/assets/javascripts/lib/utils/highlight.js @@ -27,14 +27,14 @@ export default function highlight(string, match = '', matchPrefix = '<b>', match const sanitizedValue = sanitize(string.toString(), { allowedTags: [] }); - // occurences is an array of character indices that should be + // occurrences is an array of character indices that should be // highlighted in the original string, i.e. [3, 4, 5, 7] - const occurences = fuzzaldrinPlus.match(sanitizedValue, match.toString()); + const occurrences = fuzzaldrinPlus.match(sanitizedValue, match.toString()); return sanitizedValue .split('') .map((character, i) => { - if (_.contains(occurences, i)) { + if (_.contains(occurrences, i)) { return `${matchPrefix}${character}${matchSuffix}`; } diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index a2ca4b07a66..9f30a989295 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -31,6 +31,7 @@ import initPerformanceBar from './performance_bar'; import initSearchAutocomplete from './search_autocomplete'; import GlFieldErrors from './gl_field_errors'; import initUserPopovers from './user_popovers'; +import { __ } from './locale'; // expose jQuery as global (TODO: remove these) window.jQuery = jQuery; @@ -136,10 +137,22 @@ function deferredInitialisation() { loadAwardsHandler(); - // Toggle Canary Badge + /** + * Toggle Canary Badge + * + * For GitLab.com only, when the user is using canary + * we render a Next badge and hide the option to switch + * to canay + */ if (Cookies.get('gitlab_canary') && Cookies.get('gitlab_canary') === 'true') { - document.querySelector('.js-canary-badge').classList.remove('hidden'); - document.querySelector('.js-canary-link').classList.add('hidden'); + const canaryBadge = document.querySelector('.js-canary-badge'); + const canaryLink = document.querySelector('.js-canary-link'); + if (canaryBadge) { + canaryBadge.classList.remove('hidden'); + } + if (canaryLink) { + canaryLink.classList.add('hidden'); + } } } @@ -207,9 +220,9 @@ document.addEventListener('DOMContentLoaded', () => { const ref = xhrObj.status; if (ref === 401) { - Flash('You need to be logged in.'); + Flash(__('You need to be logged in.')); } else if (ref === 404 || ref === 500) { - Flash('Something went wrong on our end.'); + Flash(__('Something went wrong on our end.')); } }); diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 509f19e6f00..e5cf43e8289 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -21,6 +21,7 @@ import { localTimeAgo } from './lib/utils/datetime_utility'; import syntaxHighlight from './syntax_highlight'; import Notes from './notes'; import { polyfillSticky } from './lib/utils/sticky'; +import { __ } from './locale'; // MergeRequestTabs // @@ -326,7 +327,7 @@ export default class MergeRequestTabs { }) .catch(() => { this.toggleLoading(false); - flash('An error occurred while fetching this tab.'); + flash(__('An error occurred while fetching this tab.')); }); } @@ -416,7 +417,7 @@ export default class MergeRequestTabs { }) .catch(() => { this.toggleLoading(false); - flash('An error occurred while fetching this tab.'); + flash(__('An error occurred while fetching this tab.')); }); } diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index f211632cf24..6aaba4e7c74 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import axios from './lib/utils/axios_utils'; import flash from './flash'; import { mouseenter, debouncedMouseleave, togglePopover } from './shared/popover'; +import { __ } from './locale'; export default class Milestone { constructor() { @@ -42,7 +43,7 @@ export default class Milestone { $(tabElId).html(data.html); $target.addClass('is-loaded'); }) - .catch(() => flash('Error loading milestone tab')); + .catch(() => flash(__('Error loading milestone tab'))); } } diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 75c18a9b6a0..43949d5cc86 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -56,14 +56,15 @@ export default class MilestoneSelect { const $value = $block.find('.value'); const $loading = $block.find('.block-loading').fadeOut(); selectedMilestoneDefault = showAny ? '' : null; - selectedMilestoneDefault = showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault; + selectedMilestoneDefault = + showNo && defaultNo ? __('No Milestone') : selectedMilestoneDefault; selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault; if (issueUpdateURL) { milestoneLinkTemplate = _.template( '<a href="<%- web_url %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>', ); - milestoneLinkNoneTemplate = '<span class="no-value">None</span>'; + milestoneLinkNoneTemplate = `<span class="no-value">${__('None')}</span>`; } return $dropdown.glDropdown({ showMenuAbove: showMenuAbove, @@ -74,28 +75,28 @@ export default class MilestoneSelect { extraOptions.push({ id: null, name: null, - title: 'Any Milestone', + title: __('Any Milestone'), }); } if (showNo) { extraOptions.push({ id: -1, - name: 'No Milestone', - title: 'No Milestone', + name: __('No Milestone'), + title: __('No Milestone'), }); } if (showUpcoming) { extraOptions.push({ id: -2, name: '#upcoming', - title: 'Upcoming', + title: __('Upcoming'), }); } if (showStarted) { extraOptions.push({ id: -3, name: '#started', - title: 'Started', + title: __('Started'), }); } if (extraOptions.length) { diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js index 81ab9d8be4b..b39ad764f01 100644 --- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js @@ -1,6 +1,7 @@ import $ from 'jquery'; import flash from './flash'; import axios from './lib/utils/axios_utils'; +import { __ } from './locale'; /** * In each pipelines table we have a mini pipeline graph for each pipeline. @@ -98,7 +99,7 @@ export default class MiniPipelineGraph { ) { $(button).dropdown('toggle'); } - flash('An error occurred while fetching the builds.', 'alert'); + flash(__('An error occurred while fetching the builds.'), 'alert'); }); } diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 33f6afc9c2d..ff1e1805948 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -1,5 +1,12 @@ <script> -import { GlDropdown, GlDropdownItem, GlLink } from '@gitlab/ui'; +import { + GlButton, + GlDropdown, + GlDropdownItem, + GlModal, + GlModalDirective, + GlLink, +} from '@gitlab/ui'; import _ from 'underscore'; import { s__ } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; @@ -23,12 +30,21 @@ export default { GraphGroup, EmptyState, Icon, + GlButton, GlDropdown, GlDropdownItem, GlLink, + GlModal, + }, + directives: { + GlModalDirective, }, - props: { + externalDashboardPath: { + type: String, + required: false, + default: '', + }, hasMetrics: { type: Boolean, required: false, @@ -96,6 +112,19 @@ export default { type: Boolean, required: true, }, + customMetricsAvailable: { + type: Boolean, + required: false, + default: false, + }, + customMetricsPath: { + type: String, + required: true, + }, + validateQueryPath: { + type: String, + required: true, + }, }, data() { return { @@ -105,8 +134,14 @@ export default { elWidth: 0, selectedTimeWindow: '', selectedTimeWindowKey: '', + formIsValid: null, }; }, + computed: { + canAddMetrics() { + return this.customMetricsAvailable && this.customMetricsPath.length; + }, + }, created() { this.service = new MonitoringService({ metricsEndpoint: this.metricsEndpoint, @@ -187,11 +222,20 @@ export default { this.state = 'unableToConnect'; }); }, + hideAddMetricModal() { + this.$refs.addMetricModal.hide(); + }, onSidebarMutation() { setTimeout(() => { this.elWidth = this.$el.clientWidth; }, sidebarAnimationDuration); }, + setFormValidity(isValid) { + this.formIsValid = isValid; + }, + submitCustomMetricsForm() { + this.$refs.customMetricsForm.submit(); + }, activeTimeWindow(key) { return this.timeWindows[key] === this.selectedTimeWindow; }, @@ -199,47 +243,96 @@ export default { return `?time_window=${key}`; }, }, + addMetric: { + title: s__('Metrics|Add metric'), + modalId: 'add-metric', + }, }; </script> <template> - <div v-if="!showEmptyState" class="prometheus-graphs prepend-top-default"> - <div - v-if="environmentsEndpoint" - class="dropdowns d-flex align-items-center justify-content-between" - > - <div class="d-flex align-items-center"> - <strong>{{ s__('Metrics|Environment') }}</strong> - <gl-dropdown - class="prepend-left-10 js-environments-dropdown" - toggle-class="dropdown-menu-toggle" - :text="currentEnvironmentName" - :disabled="store.environmentsData.length === 0" - > - <gl-dropdown-item - v-for="environment in store.environmentsData" - :key="environment.id" - :href="environment.metrics_path" - :active="environment.name === currentEnvironmentName" - active-class="is-active" - >{{ environment.name }}</gl-dropdown-item + <div v-if="!showEmptyState" class="prometheus-graphs"> + <div class="gl-p-3 border-bottom bg-gray-light d-flex justify-content-between"> + <div + v-if="environmentsEndpoint" + class="dropdowns d-flex align-items-center justify-content-between" + > + <div class="d-flex align-items-center"> + <strong>{{ s__('Metrics|Environment') }}</strong> + <gl-dropdown + class="prepend-left-10 js-environments-dropdown" + toggle-class="dropdown-menu-toggle" + :text="currentEnvironmentName" + :disabled="store.environmentsData.length === 0" + > + <gl-dropdown-item + v-for="environment in store.environmentsData" + :key="environment.id" + :active="environment.name === currentEnvironmentName" + active-class="is-active" + >{{ environment.name }}</gl-dropdown-item + > + </gl-dropdown> + </div> + <div v-if="showTimeWindowDropdown" class="d-flex align-items-center"> + <strong>{{ s__('Metrics|Show last') }}</strong> + <gl-dropdown + class="prepend-left-10 js-time-window-dropdown" + toggle-class="dropdown-menu-toggle" + :text="selectedTimeWindow" > - </gl-dropdown> + <gl-dropdown-item + v-for="(value, key) in timeWindows" + :key="key" + :active="activeTimeWindow(key)" + ><gl-link :href="setTimeWindowParameter(key)">{{ value }}</gl-link></gl-dropdown-item + > + </gl-dropdown> + </div> </div> - <div v-if="showTimeWindowDropdown" class="d-flex align-items-center"> - <strong>{{ s__('Metrics|Show last') }}</strong> - <gl-dropdown - class="prepend-left-10 js-time-window-dropdown" - toggle-class="dropdown-menu-toggle" - :text="selectedTimeWindow" - > - <gl-dropdown-item - v-for="(value, key) in timeWindows" - :key="key" - :active="activeTimeWindow(key)" - ><gl-link :href="setTimeWindowParameter(key)">{{ value }}</gl-link></gl-dropdown-item + <div class="d-flex"> + <div v-if="isEE && canAddMetrics"> + <gl-button + v-gl-modal-directive="$options.addMetric.modalId" + class="js-add-metric-button text-success border-success" + > + {{ $options.addMetric.title }} + </gl-button> + <gl-modal + ref="addMetricModal" + :modal-id="$options.addMetric.modalId" + :title="$options.addMetric.title" > - </gl-dropdown> + <form ref="customMetricsForm" :action="customMetricsPath" method="post"> + <custom-metrics-form-fields + :validate-query-path="validateQueryPath" + form-operation="post" + @formValidation="setFormValidity" + /> + </form> + <div slot="modal-footer"> + <gl-button @click="hideAddMetricModal"> + {{ __('Cancel') }} + </gl-button> + <gl-button + :disabled="!formIsValid" + variant="success" + @click="submitCustomMetricsForm" + > + {{ __('Save changes') }} + </gl-button> + </div> + </gl-modal> + </div> + <gl-button + v-if="externalDashboardPath.length" + class="js-external-dashboard-link prepend-left-8" + variant="primary" + :href="externalDashboardPath" + > + {{ __('View full dashboard') }} + <icon name="external-link" /> + </gl-button> </div> </div> <graph-group diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js index ee1a5274ff7..03d349ac714 100644 --- a/app/assets/javascripts/namespace_select.js +++ b/app/assets/javascripts/namespace_select.js @@ -4,6 +4,7 @@ import $ from 'jquery'; import Api from './api'; import { mergeUrlParams } from './lib/utils/url_utility'; import { parseBoolean } from '~/lib/utils/common_utils'; +import { __ } from './locale'; export default class NamespaceSelect { constructor(opts) { @@ -29,7 +30,7 @@ export default class NamespaceSelect { return Api.namespaces(term, function(namespaces) { if (isFilter) { const anyNamespace = { - text: 'Any namespace', + text: __('Any namespace'), id: null, }; namespaces.unshift(anyNamespace); diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 36725e22365..d03f4508fb8 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -35,6 +35,7 @@ import { } from './lib/utils/common_utils'; import imageDiffHelper from './image_diff/helpers/index'; import { localTimeAgo } from './lib/utils/datetime_utility'; +import { sprintf, s__, __ } from './locale'; window.autosize = Autosize; @@ -253,7 +254,7 @@ export default class Notes { discussionNoteForm = $textarea.closest('.js-discussion-note-form'); if (discussionNoteForm.length) { if ($textarea.val() !== '') { - if (!window.confirm('Are you sure you want to cancel creating this comment?')) { + if (!window.confirm(__('Are you sure you want to cancel creating this comment?'))) { return; } } @@ -265,7 +266,7 @@ export default class Notes { originalText = $textarea.closest('form').data('originalNote'); newText = $textarea.val(); if (originalText !== newText) { - if (!window.confirm('Are you sure you want to cancel editing this comment?')) { + if (!window.confirm(__('Are you sure you want to cancel editing this comment?'))) { return; } } @@ -636,7 +637,7 @@ export default class Notes { this.glForm = new GLForm(form, enableGFM); textarea = form.find('.js-note-text'); key = [ - 'Note', + s__('NoteForm|Note'), form.find('#note_noteable_type').val(), form.find('#note_noteable_id').val(), form.find('#note_commit_id').val(), @@ -670,7 +671,9 @@ export default class Notes { formParentTimeline = $form.closest('.discussion-notes').find('.notes'); } return this.addFlash( - 'Your comment could not be submitted! Please check your network connection and try again.', + __( + 'Your comment could not be submitted! Please check your network connection and try again.', + ), 'alert', formParentTimeline.get(0), ); @@ -679,7 +682,7 @@ export default class Notes { updateNoteError($parentTimeline) { // eslint-disable-next-line no-new new Flash( - 'Your comment could not be updated! Please check your network connection and try again.', + __('Your comment could not be updated! Please check your network connection and try again.'), ); } @@ -1258,12 +1261,19 @@ export default class Notes { putConflictEditWarningInPlace(noteEntity, $note) { if ($note.find('.js-conflict-edit-warning').length === 0) { + const open_link = `<a href="#note_${ + noteEntity.id + }" target="_blank" rel="noopener noreferrer">`; const $alert = $(`<div class="js-conflict-edit-warning alert alert-danger"> - This comment has changed since you started editing, please review the - <a href="#note_${noteEntity.id}" target="_blank" rel="noopener noreferrer"> - updated comment - </a> - to ensure information is not lost + ${sprintf( + s__( + 'Notes|This comment has changed since you started editing, please review the %{open_link}updated comment%{close_link} to ensure information is not lost', + ), + { + open_link, + close_link: '</a>', + }, + )} </div>`); $alert.insertAfter($note.find('.note-text')); } @@ -1491,13 +1501,15 @@ export default class Notes { if (executedCommands && executedCommands.length) { if (executedCommands.length > 1) { - tempFormContent = 'Applying multiple commands'; + tempFormContent = __('Applying multiple commands'); } else { const commandDescription = executedCommands[0].description.toLowerCase(); - tempFormContent = `Applying command to ${commandDescription}`; + tempFormContent = sprintf(__('Applying command to %{commandDescription}'), { + commandDescription, + }); } } else { - tempFormContent = 'Applying command'; + tempFormContent = __('Applying command'); } return tempFormContent; @@ -1817,7 +1829,9 @@ export default class Notes { $editingNote .find('.note-headline-meta a') .html( - '<i class="fa fa-spinner fa-spin" aria-label="Comment is being updated" aria-hidden="true"></i>', + `<i class="fa fa-spinner fa-spin" aria-label="${__( + 'Comment is being updated', + )}" aria-hidden="true"></i>`, ); // Make request to update comment on server diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue index 8ddd5b8514a..88454c3fb4c 100644 --- a/app/assets/javascripts/notes/components/note_body.vue +++ b/app/assets/javascripts/notes/components/note_body.vue @@ -83,10 +83,12 @@ export default { formCancelHandler(shouldConfirm, isDirty) { this.$emit('cancelForm', shouldConfirm, isDirty); }, - applySuggestion({ suggestionId, flashContainer, callback }) { + applySuggestion({ suggestionId, flashContainer, callback = () => {} }) { const { discussion_id: discussionId, id: noteId } = this.note; - this.submitSuggestion({ discussionId, noteId, suggestionId, flashContainer, callback }); + return this.submitSuggestion({ discussionId, noteId, suggestionId, flashContainer }).then( + callback, + ); }, }, }; diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 970e6551092..63658d49a05 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -142,6 +142,23 @@ export const createNewNote = ({ commit, dispatch }, { endpoint, data }) => export const removePlaceholderNotes = ({ commit }) => commit(types.REMOVE_PLACEHOLDER_NOTES); +export const resolveDiscussion = ({ state, dispatch, getters }, { discussionId }) => { + const discussion = utils.findNoteObjectById(state.discussions, discussionId); + const isResolved = getters.isDiscussionResolved(discussionId); + + if (!discussion) { + return Promise.reject(); + } else if (isResolved) { + return Promise.resolve(); + } + + return dispatch('toggleResolveNote', { + endpoint: discussion.resolve_path, + isResolved, + discussion: true, + }); +}; + export const toggleResolveNote = ({ commit, dispatch }, { endpoint, isResolved, discussion }) => service .toggleResolveNote(endpoint, isResolved) @@ -251,11 +268,20 @@ export const saveNote = ({ commit, dispatch }, noteData) => { const { errors } = res; const commandsChanges = res.commands_changes; - if (hasQuickActions && errors && Object.keys(errors).length) { - eTagPoll.makeRequest(); + if (errors && Object.keys(errors).length) { + /* + The following reply means that quick actions have been successfully applied: + + {"commands_changes":{},"valid":false,"errors":{"commands_only":["Commands applied"]}} + */ + if (hasQuickActions) { + eTagPoll.makeRequest(); - $('.js-gfm-input').trigger('clear-commands-cache.atwho'); - Flash(__('Commands applied'), 'notice', noteData.flashContainer); + $('.js-gfm-input').trigger('clear-commands-cache.atwho'); + Flash(__('Commands applied'), 'notice', noteData.flashContainer); + } else { + throw new Error(__('Failed to save comment!')); + } } if (commandsChanges) { @@ -420,15 +446,13 @@ export const updateResolvableDiscussonsCounts = ({ commit }) => commit(types.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS); export const submitSuggestion = ( - { commit }, - { discussionId, noteId, suggestionId, flashContainer, callback }, -) => { + { commit, dispatch }, + { discussionId, noteId, suggestionId, flashContainer }, +) => service .applySuggestion(suggestionId) - .then(() => { - commit(types.APPLY_SUGGESTION, { discussionId, noteId, suggestionId }); - callback(); - }) + .then(() => commit(types.APPLY_SUGGESTION, { discussionId, noteId, suggestionId })) + .then(() => dispatch('resolveDiscussion', { discussionId }).catch(() => {})) .catch(err => { const defaultMessage = __( 'Something went wrong while applying the suggestion. Please try again.', @@ -436,9 +460,7 @@ export const submitSuggestion = ( const flashMessage = err.response.data ? `${err.response.data.message}.` : defaultMessage; Flash(__(flashMessage), 'alert', flashContainer); - callback(); }); -}; export const convertToDiscussion = ({ commit }, noteId) => commit(types.CONVERT_TO_DISCUSSION, noteId); diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js index 029fde348fb..ed4cef4a917 100644 --- a/app/assets/javascripts/notes/stores/utils.js +++ b/app/assets/javascripts/notes/stores/utils.js @@ -2,7 +2,8 @@ import AjaxCache from '~/lib/utils/ajax_cache'; import { trimFirstCharOfLineContent } from '~/diffs/store/utils'; import { sprintf, __ } from '~/locale'; -const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm; +// factory function because global flag makes RegExp stateful +const createQuickActionsRegex = () => /^\/\w+.*$/gm; export const findNoteObjectById = (notes, id) => notes.filter(n => n.id === id)[0]; @@ -27,9 +28,9 @@ export const getQuickActionText = note => { return text; }; -export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note); +export const hasQuickActions = note => createQuickActionsRegex().test(note); -export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim(); +export const stripQuickActions = note => note.replace(createQuickActionsRegex(), '').trim(); export const prepareDiffLines = diffLines => diffLines.map(line => ({ ...trimFirstCharOfLineContent(line) })); diff --git a/app/assets/javascripts/operation_settings/components/external_dashboard.vue b/app/assets/javascripts/operation_settings/components/external_dashboard.vue new file mode 100644 index 00000000000..0a87d193b72 --- /dev/null +++ b/app/assets/javascripts/operation_settings/components/external_dashboard.vue @@ -0,0 +1,57 @@ +<script> +import { GlButton, GlFormGroup, GlFormInput, GlLink } from '@gitlab/ui'; + +export default { + components: { + GlButton, + GlFormGroup, + GlFormInput, + GlLink, + }, + props: { + externalDashboardPath: { + type: String, + required: false, + default: '', + }, + externalDashboardHelpPagePath: { + type: String, + required: true, + }, + }, +}; +</script> + +<template> + <section class="settings expanded"> + <div class="settings-header"> + <h4 class="js-section-header"> + {{ s__('ExternalMetrics|External Dashboard') }} + </h4> + <p class="js-section-sub-header"> + {{ + s__( + 'ExternalMetrics|Add a button to the metrics dashboard linking directly to your existing external dashboards.', + ) + }} + <gl-link :href="externalDashboardHelpPagePath">{{ __('Learn more') }}</gl-link> + </p> + </div> + <div class="settings-content"> + <form> + <gl-form-group + :label="s__('ExternalMetrics|Full dashboard URL')" + :description="s__('ExternalMetrics|Enter the URL of the dashboard you want to link to')" + > + <gl-form-input + :value="externalDashboardPath" + placeholder="https://my-org.gitlab.io/my-dashboards" + /> + </gl-form-group> + <gl-button variant="success"> + {{ __('Save Changes') }} + </gl-button> + </form> + </div> + </section> +</template> diff --git a/app/assets/javascripts/operation_settings/index.js b/app/assets/javascripts/operation_settings/index.js new file mode 100644 index 00000000000..1171f3ece9f --- /dev/null +++ b/app/assets/javascripts/operation_settings/index.js @@ -0,0 +1,26 @@ +import Vue from 'vue'; +import ExternalDashboardForm from './components/external_dashboard.vue'; + +export default () => { + /** + * This check can be removed when we remove + * the :grafana_dashboard_link feature flag + */ + if (!gon.features.grafanaDashboardLink) { + return null; + } + + const el = document.querySelector('.js-operation-settings'); + + return new Vue({ + el, + render(createElement) { + return createElement(ExternalDashboardForm, { + props: { + ...el.dataset, + expanded: false, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/pages/admin/clusters/destroy/index.js b/app/assets/javascripts/pages/admin/clusters/destroy/index.js new file mode 100644 index 00000000000..8001d2dd1da --- /dev/null +++ b/app/assets/javascripts/pages/admin/clusters/destroy/index.js @@ -0,0 +1,5 @@ +import ClustersBundle from '~/clusters/clusters_bundle'; + +document.addEventListener('DOMContentLoaded', () => { + new ClustersBundle(); // eslint-disable-line no-new +}); diff --git a/app/assets/javascripts/pages/admin/clusters/edit/index.js b/app/assets/javascripts/pages/admin/clusters/edit/index.js new file mode 100644 index 00000000000..8001d2dd1da --- /dev/null +++ b/app/assets/javascripts/pages/admin/clusters/edit/index.js @@ -0,0 +1,5 @@ +import ClustersBundle from '~/clusters/clusters_bundle'; + +document.addEventListener('DOMContentLoaded', () => { + new ClustersBundle(); // eslint-disable-line no-new +}); diff --git a/app/assets/javascripts/pages/admin/clusters/index.js b/app/assets/javascripts/pages/admin/clusters/index.js new file mode 100644 index 00000000000..d0c9ae66c6a --- /dev/null +++ b/app/assets/javascripts/pages/admin/clusters/index.js @@ -0,0 +1,21 @@ +import PersistentUserCallout from '~/persistent_user_callout'; +import initGkeDropdowns from '~/projects/gke_cluster_dropdowns'; + +function initGcpSignupCallout() { + const callout = document.querySelector('.gcp-signup-offer'); + PersistentUserCallout.factory(callout); +} + +document.addEventListener('DOMContentLoaded', () => { + const { page } = document.body.dataset; + const newClusterViews = [ + 'admin:clusters:new', + 'admin:clusters:create_gcp', + 'admin:clusters:create_user', + ]; + + if (newClusterViews.indexOf(page) > -1) { + initGcpSignupCallout(); + initGkeDropdowns(); + } +}); diff --git a/app/assets/javascripts/pages/admin/clusters/index/index.js b/app/assets/javascripts/pages/admin/clusters/index/index.js new file mode 100644 index 00000000000..30d519d0e37 --- /dev/null +++ b/app/assets/javascripts/pages/admin/clusters/index/index.js @@ -0,0 +1,6 @@ +import PersistentUserCallout from '~/persistent_user_callout'; + +document.addEventListener('DOMContentLoaded', () => { + const callout = document.querySelector('.gcp-signup-offer'); + PersistentUserCallout.factory(callout); +}); diff --git a/app/assets/javascripts/pages/admin/clusters/show/index.js b/app/assets/javascripts/pages/admin/clusters/show/index.js new file mode 100644 index 00000000000..8001d2dd1da --- /dev/null +++ b/app/assets/javascripts/pages/admin/clusters/show/index.js @@ -0,0 +1,5 @@ +import ClustersBundle from '~/clusters/clusters_bundle'; + +document.addEventListener('DOMContentLoaded', () => { + new ClustersBundle(); // eslint-disable-line no-new +}); diff --git a/app/assets/javascripts/pages/profiles/keys/index.js b/app/assets/javascripts/pages/profiles/keys/index.js index 1cd3ee1dfdb..d3dcd21f456 100644 --- a/app/assets/javascripts/pages/profiles/keys/index.js +++ b/app/assets/javascripts/pages/profiles/keys/index.js @@ -2,6 +2,8 @@ import AddSshKeyValidation from '~/profile/add_ssh_key_validation'; document.addEventListener('DOMContentLoaded', () => { const input = document.querySelector('.js-add-ssh-key-validation-input'); + if (!input) return; + const warning = document.querySelector('.js-add-ssh-key-validation-warning'); const originalSubmit = input.form.querySelector('.js-add-ssh-key-validation-original-submit'); const confirmSubmit = warning.querySelector('.js-add-ssh-key-validation-confirm-submit'); diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js index c1f6edf2f27..a20a0526f12 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js @@ -1,4 +1,10 @@ -const defaultTimezone = 'UTC'; +const defaultTimezone = { name: 'UTC', offset: 0 }; +const defaults = { + $inputEl: null, + $dropdownEl: null, + onSelectTimezone: null, + displayFormat: item => item.name, +}; export const formatUtcOffset = offset => { const parsed = parseInt(offset, 10); @@ -11,23 +17,28 @@ export const formatUtcOffset = offset => { export const formatTimezone = item => `[UTC ${formatUtcOffset(item.offset)}] ${item.name}`; -const defaults = { - $inputEl: null, - $dropdownEl: null, - onSelectTimezone: null, +export const findTimezoneByIdentifier = (tzList = [], identifier = null) => { + if (tzList && tzList.length && identifier && identifier.length) { + return tzList.find(tz => tz.identifier === identifier) || null; + } + return null; }; export default class TimezoneDropdown { - constructor({ $dropdownEl, $inputEl, onSelectTimezone } = defaults) { + constructor({ $dropdownEl, $inputEl, onSelectTimezone, displayFormat } = defaults) { this.$dropdown = $dropdownEl; this.$dropdownToggle = this.$dropdown.find('.dropdown-toggle-text'); this.$input = $inputEl; this.timezoneData = this.$dropdown.data('data'); + this.onSelectTimezone = onSelectTimezone; + this.displayFormat = displayFormat || defaults.displayFormat; + + this.initialTimezone = + findTimezoneByIdentifier(this.timezoneData, this.$input.val()) || defaultTimezone; + this.initDefaultTimezone(); this.initDropdown(); - - this.onSelectTimezone = onSelectTimezone; } initDropdown() { @@ -35,7 +46,7 @@ export default class TimezoneDropdown { data: this.timezoneData, filterable: true, selectable: true, - toggleLabel: item => item.name, + toggleLabel: this.displayFormat, search: { fields: ['name'], }, @@ -43,20 +54,17 @@ export default class TimezoneDropdown { text: item => formatTimezone(item), }); - this.setDropdownToggle(); + this.setDropdownToggle(this.displayFormat(this.initialTimezone)); } initDefaultTimezone() { - const initialValue = this.$input.val(); - - if (!initialValue) { - this.$input.val(defaultTimezone); + if (!this.$input.val()) { + this.$input.val(defaultTimezone.name); } } - setDropdownToggle() { - const initialValue = this.$input.val(); - this.$dropdownToggle.text(initialValue); + setDropdownToggle(dropdownText) { + this.$dropdownToggle.text(dropdownText); } updateInputValue({ selectedObj, e }) { diff --git a/app/assets/javascripts/pages/projects/settings/operations/show/index.js b/app/assets/javascripts/pages/projects/settings/operations/show/index.js index 73c745179be..5270a7924ec 100644 --- a/app/assets/javascripts/pages/projects/settings/operations/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/operations/show/index.js @@ -1,5 +1,7 @@ import mountErrorTrackingForm from '~/error_tracking_settings'; +import mountOperationSettings from '~/operation_settings'; document.addEventListener('DOMContentLoaded', () => { mountErrorTrackingForm(); + mountOperationSettings(); }); diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js index 869f70e7d33..6aa41d0825b 100644 --- a/app/assets/javascripts/pages/projects/show/index.js +++ b/app/assets/javascripts/pages/projects/show/index.js @@ -46,4 +46,12 @@ document.addEventListener('DOMContentLoaded', () => { GpgBadges.fetch(); leaveByUrl('project'); + + if (document.getElementById('js-tree-list')) { + import('~/repository') + .then(m => m.default()) + .catch(e => { + throw e; + }); + } }); diff --git a/app/assets/javascripts/pages/projects/tree/show/index.js b/app/assets/javascripts/pages/projects/tree/show/index.js index 400aed35e32..7b90a3a4f6e 100644 --- a/app/assets/javascripts/pages/projects/tree/show/index.js +++ b/app/assets/javascripts/pages/projects/tree/show/index.js @@ -40,4 +40,12 @@ document.addEventListener('DOMContentLoaded', () => { } GpgBadges.fetch(); + + if (document.getElementById('js-tree-list')) { + import('~/repository') + .then(m => m.default()) + .catch(e => { + throw e; + }); + } }); diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js index deacff5abe7..6e3800021b4 100644 --- a/app/assets/javascripts/profile/profile.js +++ b/app/assets/javascripts/profile/profile.js @@ -2,6 +2,9 @@ import $ from 'jquery'; import axios from '~/lib/utils/axios_utils'; import flash from '../flash'; import { parseBoolean } from '~/lib/utils/common_utils'; +import TimezoneDropdown, { + formatTimezone, +} from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown'; export default class Profile { constructor({ form } = {}) { @@ -10,6 +13,14 @@ export default class Profile { this.setRepoRadio(); this.bindEvents(); this.initAvatarGlCrop(); + + this.$inputEl = $('#user_timezone'); + + this.timezoneDropdown = new TimezoneDropdown({ + $inputEl: this.$inputEl, + $dropdownEl: $('.js-timezone-dropdown'), + displayFormat: selectedItem => formatTimezone(selectedItem), + }); } initAvatarGlCrop() { diff --git a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js index 40a873833e1..41e295387ae 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js +++ b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js @@ -1,3 +1,5 @@ +import { __ } from '~/locale'; + export default class ProtectedBranchAccessDropdown { constructor(options) { this.options = options; @@ -15,7 +17,7 @@ export default class ProtectedBranchAccessDropdown { if ($el.is('.is-active')) { return item.text; } - return 'Select'; + return __('Select'); }, clicked(options) { options.e.preventDefault(); diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js index 48343c8ba0a..16ecd5523d6 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_create.js +++ b/app/assets/javascripts/protected_branches/protected_branch_create.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown'; import CreateItemDropdown from '../create_item_dropdown'; import AccessorUtilities from '../lib/utils/accessor'; +import { __ } from '~/locale'; export default class ProtectedBranchCreate { constructor() { @@ -35,7 +36,7 @@ export default class ProtectedBranchCreate { this.createItemDropdown = new CreateItemDropdown({ $dropdown: $protectedBranchDropdown, - defaultToggleLabel: 'Protected Branch', + defaultToggleLabel: __('Protected Branch'), fieldName: 'protected_branch[name]', onSelect: this.onSelectCallback, getData: ProtectedBranchCreate.getProtectedBranches, diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js index 5bc08f60d16..08d8c9919dd 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit.js +++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js @@ -1,6 +1,7 @@ import flash from '../flash'; import axios from '../lib/utils/axios_utils'; import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown'; +import { __ } from '~/locale'; export default class ProtectedBranchEdit { constructor(options) { @@ -68,7 +69,7 @@ export default class ProtectedBranchEdit { this.$allowedToPushDropdown.enable(); flash( - 'Failed to update branch!', + __('Failed to update branch!'), 'alert', document.querySelector('.js-protected-branches-list'), ); diff --git a/app/assets/javascripts/reports/components/issues_list.vue b/app/assets/javascripts/reports/components/issues_list.vue index f4243522ef8..50f2910e02d 100644 --- a/app/assets/javascripts/reports/components/issues_list.vue +++ b/app/assets/javascripts/reports/components/issues_list.vue @@ -52,6 +52,11 @@ export default { required: false, default: '', }, + showReportSectionStatus: { + type: Boolean, + required: false, + default: true, + }, }, computed: { issuesWithState() { @@ -81,6 +86,7 @@ export default { :status="wrapped.status" :component="component" :is-new="wrapped.isNew" + :show-report-section-status="showReportSectionStatus" /> </smart-virtual-list> </template> diff --git a/app/assets/javascripts/reports/components/report_item.vue b/app/assets/javascripts/reports/components/report_item.vue index d2106f9ad2e..01a30809e1a 100644 --- a/app/assets/javascripts/reports/components/report_item.vue +++ b/app/assets/javascripts/reports/components/report_item.vue @@ -34,12 +34,22 @@ export default { required: false, default: false, }, + showReportSectionStatusIcon: { + type: Boolean, + required: false, + default: true, + }, }, }; </script> <template> <li :class="{ 'is-dismissed': issue.isDismissed }" class="report-block-list-issue"> - <issue-status-icon :status="status" :status-icon-size="statusIconSize" class="append-right-5" /> + <issue-status-icon + v-if="showReportSectionStatusIcon" + :status="status" + :status-icon-size="statusIconSize" + class="append-right-5" + /> <component :is="component" v-if="component" :issue="issue" :status="status" :is-new="isNew" /> </li> diff --git a/app/assets/javascripts/reports/components/report_section.vue b/app/assets/javascripts/reports/components/report_section.vue index d6483e95278..420e71f5e86 100644 --- a/app/assets/javascripts/reports/components/report_section.vue +++ b/app/assets/javascripts/reports/components/report_section.vue @@ -73,6 +73,11 @@ export default { default: () => ({}), required: false, }, + showReportSectionStatusIcon: { + type: Boolean, + required: false, + default: true, + }, }, data() { @@ -166,6 +171,7 @@ export default { :resolved-issues="resolvedIssues" :neutral-issues="neutralIssues" :component="component" + :show-report-section-status-icon="showReportSectionStatusIcon" /> </slot> </div> diff --git a/app/assets/javascripts/reports/store/state.js b/app/assets/javascripts/reports/store/state.js index 5484900276c..25f9f70d095 100644 --- a/app/assets/javascripts/reports/store/state.js +++ b/app/assets/javascripts/reports/store/state.js @@ -40,6 +40,11 @@ export default () => ({ text: s__('Reports|Class'), type: fieldTypes.link, }, + classname: { + value: null, + text: s__('Reports|Classname'), + type: fieldTypes.text, + }, execution_time: { value: null, text: s__('Reports|Execution time'), diff --git a/app/assets/javascripts/repository/components/app.vue b/app/assets/javascripts/repository/components/app.vue new file mode 100644 index 00000000000..98240aef810 --- /dev/null +++ b/app/assets/javascripts/repository/components/app.vue @@ -0,0 +1,3 @@ +<template> + <router-view /> +</template> diff --git a/app/assets/javascripts/repository/graphql.js b/app/assets/javascripts/repository/graphql.js new file mode 100644 index 00000000000..febfcce780c --- /dev/null +++ b/app/assets/javascripts/repository/graphql.js @@ -0,0 +1,11 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; + +Vue.use(VueApollo); + +const defaultClient = createDefaultClient({}); + +export default new VueApollo({ + defaultClient, +}); diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js new file mode 100644 index 00000000000..00b69362312 --- /dev/null +++ b/app/assets/javascripts/repository/index.js @@ -0,0 +1,25 @@ +import Vue from 'vue'; +import createRouter from './router'; +import App from './components/app.vue'; +import apolloProvider from './graphql'; + +export default function setupVueRepositoryList() { + const el = document.getElementById('js-tree-list'); + const { projectPath, ref } = el.dataset; + + apolloProvider.clients.defaultClient.cache.writeData({ + data: { + projectPath, + ref, + }, + }); + + return new Vue({ + el, + router: createRouter(projectPath, ref), + apolloProvider, + render(h) { + return h(App); + }, + }); +} diff --git a/app/assets/javascripts/repository/pages/index.vue b/app/assets/javascripts/repository/pages/index.vue new file mode 100644 index 00000000000..fdbf195f0f6 --- /dev/null +++ b/app/assets/javascripts/repository/pages/index.vue @@ -0,0 +1,24 @@ +<script> +import getRef from '../queries/getRef.graphql'; + +export default { + apollo: { + ref: { + query: getRef, + }, + }, + data() { + return { + ref: '', + }; + }, +}; +</script> + +<template> + <div> + <router-link :to="{ path: `/tree/${ref}/app` }"> + Go to tree + </router-link> + </div> +</template> diff --git a/app/assets/javascripts/repository/pages/tree.vue b/app/assets/javascripts/repository/pages/tree.vue new file mode 100644 index 00000000000..f51aafee775 --- /dev/null +++ b/app/assets/javascripts/repository/pages/tree.vue @@ -0,0 +1,15 @@ +<script> +export default { + props: { + path: { + type: String, + required: false, + default: '/', + }, + }, +}; +</script> + +<template> + <div>{{ path }}</div> +</template> diff --git a/app/assets/javascripts/repository/queries/getRef.graphql b/app/assets/javascripts/repository/queries/getRef.graphql new file mode 100644 index 00000000000..58c09844c3f --- /dev/null +++ b/app/assets/javascripts/repository/queries/getRef.graphql @@ -0,0 +1,3 @@ +query getRef { + ref @client +} diff --git a/app/assets/javascripts/repository/router.js b/app/assets/javascripts/repository/router.js new file mode 100644 index 00000000000..b42a96a4ee2 --- /dev/null +++ b/app/assets/javascripts/repository/router.js @@ -0,0 +1,36 @@ +import Vue from 'vue'; +import VueRouter from 'vue-router'; +import { joinPaths } from '../lib/utils/url_utility'; +import IndexPage from './pages/index.vue'; +import TreePage from './pages/tree.vue'; + +Vue.use(VueRouter); + +export default function createRouter(base, baseRef) { + return new VueRouter({ + mode: 'history', + base: joinPaths(gon.relative_url_root || '', base), + routes: [ + { + path: '/', + name: 'projectRoot', + component: IndexPage, + }, + { + path: `/tree/${baseRef}(/.*)?`, + name: 'treePath', + component: TreePage, + props: route => ({ + path: route.params.pathMatch, + }), + beforeEnter(to, from, next) { + document + .querySelectorAll('.js-hide-on-navigation') + .forEach(el => el.classList.add('hidden')); + + next(); + }, + }, + ], + }); +} diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue index c0659a0173a..10e2c8453e2 100644 --- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue +++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue @@ -178,7 +178,7 @@ export default { /> <div ref="userStatusForm" class="form-group position-relative m-0"> <div class="input-group"> - <span class="input-group-btn"> + <span class="input-group-prepend"> <button ref="toggleEmojiMenuButton" v-gl-tooltip.bottom @@ -211,7 +211,7 @@ export default { @keyup.enter.prevent @click="hideEmojiMenu" /> - <span v-show="isDirty" class="input-group-btn"> + <span v-show="isDirty" class="input-group-append"> <button v-gl-tooltip.bottom :title="s__('SetStatusModal|Clear status')" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue index da0a9483f8e..8f4cae8ae58 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue @@ -23,6 +23,8 @@ export default { TooltipOnTruncate, FilteredSearchDropdown, ReviewAppLink, + VisualReviewAppLink: () => + import('ee_component/vue_merge_request_widget/components/visual_review_app_link.vue'), }, directives: { GlTooltip: GlTooltipDirective, @@ -37,6 +39,20 @@ export default { type: Boolean, required: true, }, + showVisualReviewApp: { + type: Boolean, + required: false, + default: false, + }, + visualReviewAppMeta: { + type: Object, + required: false, + default: () => ({ + sourceProjectId: '', + issueId: '', + appUrl: '', + }), + }, }, deployedTextMap: { running: __('Deploying to'), @@ -168,6 +184,11 @@ export default { :link="deploymentExternalUrl" :css-class="`deploy-link js-deploy-url inline ${slotProps.className}`" /> + <visual-review-app-link + v-if="showVisualReviewApp" + :link="deploymentExternalUrl" + :app-metadata="visualReviewAppMeta" + /> </template> <template slot="result" slot-scope="slotProps"> @@ -187,11 +208,17 @@ export default { </a> </template> </filtered-search-dropdown> - <review-app-link - v-else - :link="deploymentExternalUrl" - css-class="js-deploy-url js-deploy-url-feature-flag deploy-link btn btn-default btn-sm inlin" - /> + <template v-else> + <review-app-link + :link="deploymentExternalUrl" + css-class="js-deploy-url js-deploy-url-feature-flag deploy-link btn btn-default btn-sm inline" + /> + <visual-review-app-link + v-if="showVisualReviewApp" + :link="deploymentExternalUrl" + :app-metadata="visualReviewAppMeta" + /> + </template> </template> <span v-if="deployment.stop_url" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue index f5a1ff2f6fd..f5fa68308bc 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue @@ -94,8 +94,8 @@ export default { </script> <template> - <div v-if="hasPipeline || hasCIError" class="ci-widget media js-ci-widget"> - <template v-if="hasCIError"> + <div class="ci-widget media js-ci-widget"> + <template v-if="!hasPipeline || hasCIError"> <div class="add-border ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-default" > diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue index 5f5fe67b3c1..b9f5f602117 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue @@ -30,9 +30,6 @@ export default { }, }, computed: { - pipeline() { - return this.isPostMerge ? this.mr.mergePipeline : this.mr.pipeline; - }, branch() { return this.isPostMerge ? this.mr.targetBranch : this.mr.sourceBranch; }, @@ -48,6 +45,19 @@ export default { hasDeploymentMetrics() { return this.isPostMerge; }, + visualReviewAppMeta() { + return { + appUrl: this.mr.appUrl, + issueId: this.mr.iid, + sourceProjectId: this.mr.sourceProjectId, + }; + }, + pipeline() { + return this.isPostMerge ? this.mr.mergePipeline : this.mr.pipeline; + }, + showVisualReviewAppLink() { + return !!(this.mr.visualReviewFF && this.mr.visualReviewAppAvailable); + }, }, }; </script> @@ -61,14 +71,18 @@ export default { :source-branch-link="branchLink" :troubleshooting-docs-path="mr.troubleshootingDocsPath" /> - <div v-if="deployments.length" slot="footer" class="mr-widget-extension"> - <deployment - v-for="deployment in deployments" - :key="deployment.id" - :class="deploymentClass" - :deployment="deployment" - :show-metrics="hasDeploymentMetrics" - /> - </div> + <template v-slot:footer> + <div v-if="deployments.length" class="mr-widget-extension"> + <deployment + v-for="deployment in deployments" + :key="deployment.id" + :class="deploymentClass" + :deployment="deployment" + :show-metrics="hasDeploymentMetrics" + :show-visual-review-app="true" + :visual-review-app-meta="visualReviewAppMeta" + /> + </div> + </template> </mr-widget-container> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue b/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue index de9c122f268..457a71cab95 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue @@ -19,6 +19,6 @@ export default { </script> <template> <a :href="link" target="_blank" rel="noopener noreferrer nofollow" :class="cssClass"> - {{ __('View app') }} <icon name="external-link" /> + {{ __('View app') }} <icon css-classes="fgray" name="external-link" /> </a> </template> diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue index 671b4909839..a620f560b52 100644 --- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue +++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue @@ -7,7 +7,7 @@ * * @example * <clipboard-button - * title="Copy to clipbard" + * title="Copy to clipboard" * text="Content to be copied" * css-class="btn-transparent" * /> diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue index c5a2aa1f2af..32783b85df4 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue @@ -1,8 +1,10 @@ <script> import Icon from '~/vue_shared/components/icon.vue'; +import { GlButton, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui'; export default { - components: { Icon }, + components: { Icon, GlButton, GlLoadingIcon }, + directives: { 'gl-tooltip': GlTooltipDirective }, props: { canApply: { type: Boolean, @@ -21,7 +23,6 @@ export default { }, data() { return { - isAppliedSuccessfully: false, isApplying: false, }; }, @@ -47,14 +48,19 @@ export default { </a> </div> <span v-if="isApplied" class="badge badge-success">{{ __('Applied') }}</span> - <button - v-if="canApply" - type="button" - class="btn qa-apply-btn" + <div v-if="isApplying" class="d-flex align-items-center text-secondary"> + <gl-loading-icon class="d-flex-center mr-2" /> + <span>{{ __('Applying suggestion') }}</span> + </div> + <gl-button + v-else-if="canApply" + v-gl-tooltip.viewport="__('This also resolves the discussion')" + class="btn-inverted qa-apply-btn" :disabled="isApplying" + variant="success" @click="applySuggestion" > {{ __('Apply suggestion') }} - </button> + </gl-button> </div> </template> diff --git a/app/assets/stylesheets/components/toast.scss b/app/assets/stylesheets/components/toast.scss index 91d16c8e98d..33e1c4e5349 100644 --- a/app/assets/stylesheets/components/toast.scss +++ b/app/assets/stylesheets/components/toast.scss @@ -1,3 +1,53 @@ -.toast-close { - font-size: $default-icon-size !important; +/* +* These styles are specific to the gl-toast component. +* Documentation: https://design.gitlab.com/components/toasts +* Note: Styles below are nested in order to override some of vue-toasted's default styling +*/ +.toasted-container { + + max-width: $toast-max-width; + + @include media-breakpoint-down(xs) { + width: 100%; + padding-right: $toast-padding-right; + } + + .toasted.gl-toast { + border-radius: $border-radius-default; + font-size: $gl-font-size; + padding: $gl-padding-8 $gl-padding-24; + margin-top: $toast-default-margin; + line-height: $gl-line-height; + background-color: rgba($gray-900, $toast-background-opacity); + + @include media-breakpoint-down(xs) { + .action:first-child { + // Ensures actions buttons are right aligned on mobile + margin-left: auto; + } + } + + .action { + color: $blue-300; + margin: 0 0 0 $toast-action-margin-left; + text-transform: none; + font-size: $gl-font-size; + + &:first-child { + padding-right: 0; + } + } + + .toast-close { + font-size: $default-icon-size; + margin-left: $toast-default-margin; + padding-left: $gl-padding; + } + } +} + +// Overrides the default positioning of toasts +body .toasted-container.bottom-left { + bottom: $toast-offset; + left: $toast-offset; } diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index e6c55252b24..3aabb66f7a6 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -22,6 +22,10 @@ } } +.oneline { + line-height: 35px; +} + .row-content-block { margin-top: 0; background-color: $gray-light; @@ -77,10 +81,6 @@ color: $gl-text-color; } - .oneline { - line-height: 35px; - } - > p:last-child { margin-bottom: 0; } @@ -108,10 +108,6 @@ padding: 11px 0; margin-bottom: 11px; - .oneline { - line-height: 35px; - } - &.no-bottom-space { border-bottom: 0; margin-bottom: 0; @@ -160,8 +156,6 @@ } .cover-desc { - color: $gl-text-color; - &.username:last-child { padding-bottom: $gl-padding; } diff --git a/app/assets/stylesheets/framework/ci_variable_list.scss b/app/assets/stylesheets/framework/ci_variable_list.scss index d9b0e4558ad..28d7492b99c 100644 --- a/app/assets/stylesheets/framework/ci_variable_list.scss +++ b/app/assets/stylesheets/framework/ci_variable_list.scss @@ -47,6 +47,7 @@ display: flex; align-items: flex-start; width: 100%; + padding-bottom: $gl-padding; @include media-breakpoint-down(xs) { display: block; diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index dffd5e70edb..fc488b85138 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -5,6 +5,9 @@ .cgreen { color: $green-600; } .cdark { color: $common-gray-dark; } +.fwhite { fill: $white-light; } +.fgray { fill: $gray-700; } + .text-plain, .text-plain:hover { color: $gl-text-color; @@ -48,6 +51,10 @@ color: $brand-info; } +.bg-gray-light { + background-color: $gray-light; +} + .text-break-word { word-break: break-all; } @@ -435,10 +442,6 @@ img.emoji { .min-height-0 { min-height: 0; } -.w-3 { width: #{3 * $grid-size}; } - -.h-3 { width: #{3 * $grid-size}; } - .svg-w-100 { svg { width: 100%; @@ -446,19 +449,13 @@ img.emoji { } /** COMMON SPACING CLASSES **/ -.gl-pl-0 { padding-left: 0; } -.gl-pl-1 { padding-left: #{0.5 * $grid-size}; } -.gl-pl-2 { padding-left: $grid-size; } -.gl-pl-3 { padding-left: #{2 * $grid-size}; } -.gl-pl-4 { padding-left: #{3 * $grid-size}; } -.gl-pl-5 { padding-left: #{4 * $grid-size}; } - -.gl-pr-0 { padding-right: 0; } -.gl-pr-1 { padding-right: #{0.5 * $grid-size}; } -.gl-pr-2 { padding-right: $grid-size; } -.gl-pr-3 { padding-right: #{2 * $grid-size}; } -.gl-pr-4 { padding-right: #{3 * $grid-size}; } -.gl-pr-5 { padding-right: #{4 * $grid-size}; } +@each $index, $padding in $spacing-scale { + #{'.gl-p-#{$index}'} { padding: $padding; } + #{'.gl-pl-#{$index}'} { padding-left: $padding; } + #{'.gl-pr-#{$index}'} { padding-right: $padding; } + #{'.gl-pt-#{$index}'} { padding-top: $padding; } + #{'.gl-pb-#{$index}'} { padding-bottom: $padding; } +} /** * Removes browser specific clear icon from input fields in diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index a6179e2a96e..1bc597bd4ae 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -472,6 +472,22 @@ background-color: $blue-500; } } + + .canary-badge { + .badge { + font-size: $gl-font-size-small; + line-height: $gl-line-height; + padding: 0 $grid-size; + } + + &:hover { + text-decoration: none; + + .badge { + text-decoration: none; + } + } + } } @include media-breakpoint-down(xs) { @@ -581,10 +597,15 @@ .emoji-menu-toggle-button { @include emoji-menu-toggle-button; + padding: $gl-vert-padding $gl-btn-padding; } .input-group { - height: 34px; + &, + .input-group-prepend, + .input-group-append { + height: $input-height; + } } } diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index 323a3dbecd5..f75e5b55506 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -53,7 +53,8 @@ flex-direction: row; .btn + .btn:not(.dropdown-toggle-split), - .btn + .btn-group { + .btn + .btn-group, + .btn-group + .btn { margin-left: $grid-size; } @@ -61,17 +62,12 @@ flex-direction: column; .btn + .btn:not(.dropdown-toggle-split), - .btn + .btn-group { + .btn + .btn-group, + .btn-group + .btn { margin-left: 0; margin-top: $grid-size; } } - - @include media-breakpoint-up(sm) { - .btn:nth-child(1) { - margin-left: auto; - } - } } body.modal-open { diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 244b414d334..7c152efd9c7 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -473,3 +473,7 @@ textarea { /* stylelint-enable */ .lh-100 { line-height: 1; } + +wbr { + display: inline-block; +} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index da1f196afdb..1cf122102cc 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -11,6 +11,14 @@ $default-transition-duration: 0.15s; $contextual-sidebar-width: 220px; $contextual-sidebar-collapsed-width: 50px; $toggle-sidebar-height: 48px; +$spacing-scale: ( + 0: 0, + 1: #{0.5 * $grid-size}, + 2: $grid-size, + 3: #{2 * $grid-size}, + 4: #{3 * $grid-size}, + 5: #{4 * $grid-size} +); /* * Color schema @@ -424,7 +432,7 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%); */ $monospace-font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace; -$regular-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, +$regular-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans', Ubuntu, Cantarell, 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; @@ -490,6 +498,17 @@ $pagination-line-height: 20px; $pagination-disabled-color: #cdcdcd; /* +* Toasts +*/ +$toast-offset: 24px; +$toast-height: 48px; +$toast-max-width: 586px; +$toast-padding-right: 42px; +$toast-default-margin: 8px; +$toast-action-margin-left: 16px; +$toast-background-opacity: 0.95; + +/* * Status icons */ $status-icon-size: 22px; diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss index e0b84e0f92d..f8e273a2735 100644 --- a/app/assets/stylesheets/pages/members.scss +++ b/app/assets/stylesheets/pages/members.scss @@ -14,7 +14,7 @@ } .member { - &.is-overriden { + &.is-overridden { .btn-ldap-override { display: none !important; } @@ -130,9 +130,6 @@ .members-ldap { align-self: center; - height: 100%; - margin-right: 10px; - margin-left: -49px; } .alert-member-ldap { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 709940ba6c8..44b558dd5ff 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -972,10 +972,6 @@ } } - .btn svg { - fill: $gray-700; - } - .dropdown-menu { width: 400px; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 09f75cd827f..f2b67a693c3 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -471,6 +471,11 @@ $note-form-margin-left: 72px; vertical-align: top; white-space: normal; + // Fixes subpixel rounding issue https://gitlab.com/gitlab-org/gitlab-ce/issues/53973 + // background-color is needed for dark code preference + padding-bottom: 1px; + background-color: $white-light; + &.parallel { border-width: 1px; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 12a3b8c88f3..aa6bbc8e473 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -903,7 +903,7 @@ button.mini-pipeline-graph-dropdown-toggle { // Match dropdown.scss for all `a` tags &.non-details-job-component { - padding: 8px 16px; + padding: $gl-padding-8 $gl-btn-horz-padding; } .ci-job-name-component { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 7778b4aab3d..151af843c95 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -1446,3 +1446,86 @@ pre.light-well { } } } + +.project-filters { + .btn svg { + color: $gl-gray-700; + } + + .button-filter-group { + .btn { + width: 96px; + } + + a { + color: $black; + } + + .active { + background: $btn-active-gray; + } + } + + .filtered-search-dropdown-label { + min-width: 68px; + + @include media-breakpoint-down(xs) { + min-width: 60px; + } + } + + .filtered-search { + min-width: 30%; + flex-basis: 0; + + .project-filter-form .project-filter-form-field { + padding-right: $gl-padding-8; + } + + .filtered-search, + .filtered-search-nav, + .filtered-search-dropdown { + flex-basis: 0; + } + + @include media-breakpoint-down(lg) { + min-width: 15%; + + .project-filter-form-field { + min-width: 150px; + } + } + + @include media-breakpoint-down(md) { + min-width: 30%; + } + } + + .filtered-search-box { + border-radius: 3px 0 0 3px; + } + + .dropdown-menu-toggle { + margin-left: $gl-padding-8; + } + + @include media-breakpoint-down(md) { + .extended-filtered-search-box { + min-width: 55%; + } + + .filtered-search-dropdown { + width: 50%; + + .dropdown-menu-toggle { + width: 100%; + } + } + } + + @include media-breakpoint-down(xs) { + .filtered-search-dropdown { + width: 100%; + } + } +} diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index 2a1e8345755..586365eb1ce 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -110,45 +110,38 @@ } .todo-body { - .todo-note { - word-wrap: break-word; - - .md { - color: $gl-grayish-blue; - font-size: $gl-font-size; - - .badge.badge-pill { - color: $gl-text-color; - } + .badge.badge-pill, + p { + color: $gl-text-color; + } - p { - color: $gl-text-color; - } - } + .md { + color: $gl-grayish-blue; + font-size: $gl-font-size; + } - code { - white-space: pre-wrap; - } + code { + white-space: pre-wrap; + } - pre { - border: 0; - background: $gray-light; - border-radius: 0; - color: $gl-gray-500; - margin: 0 20px; - overflow: hidden; - } + pre { + border: 0; + background: $gray-light; + border-radius: 0; + color: $gl-gray-500; + margin: 0 20px; + overflow: hidden; + } - .note-image-attach { - margin-top: 4px; - margin-left: 0; - max-width: 200px; - float: none; - } + .note-image-attach { + margin-top: 4px; + margin-left: 0; + max-width: 200px; + float: none; + } - p:last-child { - margin-bottom: 0; - } + p:last-child { + margin-bottom: 0; } } } diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb index ef182b981f1..b742b7e19cf 100644 --- a/app/controllers/admin/application_controller.rb +++ b/app/controllers/admin/application_controller.rb @@ -4,10 +4,7 @@ # # Automatically sets the layout and ensures an administrator is logged in class Admin::ApplicationController < ApplicationController - before_action :authenticate_admin! - layout 'admin' + include EnforcesAdminAuthentication - def authenticate_admin! - render_404 unless current_user.admin? - end + layout 'admin' end diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index d445be0eb19..d5bc723aa8c 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -89,6 +89,13 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ) end + # Getting ToS url requires `directory` api call to Let's Encrypt + # which could result in 500 error/slow rendering on settings page + # Because of that we use separate controller action + def lets_encrypt_terms_of_service + redirect_to ::Gitlab::LetsEncrypt.terms_of_service_url + end + private def set_application_setting diff --git a/app/controllers/admin/clusters/applications_controller.rb b/app/controllers/admin/clusters/applications_controller.rb new file mode 100644 index 00000000000..7400cc16175 --- /dev/null +++ b/app/controllers/admin/clusters/applications_controller.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class Admin::Clusters::ApplicationsController < Clusters::ApplicationsController + include EnforcesAdminAuthentication + + private + + def clusterable + @clusterable ||= InstanceClusterablePresenter.fabricate(Clusters::Instance.new, current_user: current_user) + end +end diff --git a/app/controllers/admin/clusters_controller.rb b/app/controllers/admin/clusters_controller.rb new file mode 100644 index 00000000000..f54933de10f --- /dev/null +++ b/app/controllers/admin/clusters_controller.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class Admin::ClustersController < Clusters::ClustersController + include EnforcesAdminAuthentication + + layout 'admin' + + private + + def clusterable + @clusterable ||= InstanceClusterablePresenter.fabricate(Clusters::Instance.new, current_user: current_user) + end +end diff --git a/app/controllers/concerns/enforces_admin_authentication.rb b/app/controllers/concerns/enforces_admin_authentication.rb new file mode 100644 index 00000000000..3ef92730df6 --- /dev/null +++ b/app/controllers/concerns/enforces_admin_authentication.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# == EnforcesAdminAuthentication +# +# Controller concern to enforce that users are authenticated as admins +# +# Upon inclusion, adds `authenticate_admin!` as a before_action +# +module EnforcesAdminAuthentication + extend ActiveSupport::Concern + + included do + before_action :authenticate_admin! + end + + def authenticate_admin! + render_404 unless current_user.admin? + end +end diff --git a/app/controllers/concerns/project_unauthorized.rb b/app/controllers/concerns/project_unauthorized.rb index d42363b8b17..7238840440f 100644 --- a/app/controllers/concerns/project_unauthorized.rb +++ b/app/controllers/concerns/project_unauthorized.rb @@ -1,10 +1,12 @@ # frozen_string_literal: true module ProjectUnauthorized - def project_unauthorized_proc - lambda do |project| - if project - label = project.external_authorization_classification_label + module ControllerActions + def self.on_routable_not_found + lambda do |routable| + return unless routable.is_a?(Project) + + label = routable.external_authorization_classification_label rejection_reason = nil unless ::Gitlab::ExternalAuthorization.access_allowed?(current_user, label) @@ -12,9 +14,7 @@ module ProjectUnauthorized rejection_reason ||= _('External authorization denied access to this project') end - if rejection_reason - access_denied!(rejection_reason) - end + access_denied!(rejection_reason) if rejection_reason end end end diff --git a/app/controllers/concerns/routable_actions.rb b/app/controllers/concerns/routable_actions.rb index 5624eb3aa45..ff9b0332c97 100644 --- a/app/controllers/concerns/routable_actions.rb +++ b/app/controllers/concerns/routable_actions.rb @@ -3,15 +3,13 @@ module RoutableActions extend ActiveSupport::Concern - def find_routable!(routable_klass, requested_full_path, extra_authorization_proc: nil, not_found_or_authorized_proc: nil) + def find_routable!(routable_klass, requested_full_path, extra_authorization_proc: nil) routable = routable_klass.find_by_full_path(requested_full_path, follow_redirects: request.get?) if routable_authorized?(routable, extra_authorization_proc) ensure_canonical_path(routable, requested_full_path) routable else - if not_found_or_authorized_proc - not_found_or_authorized_proc.call(routable) - end + perform_not_found_actions(routable, not_found_actions) route_not_found unless performed? @@ -19,6 +17,18 @@ module RoutableActions end end + def not_found_actions + [ProjectUnauthorized::ControllerActions.on_routable_not_found] + end + + def perform_not_found_actions(routable, actions) + actions.each do |action| + break if performed? + + instance_exec(routable, &action) + end + end + def routable_authorized?(routable, extra_authorization_proc) return false unless routable diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index d9b3b4bbbd9..2a8dd997d04 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -86,7 +86,8 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController log_audit_event(current_user, with: oauth['provider']) identity_linker ||= auth_module::IdentityLinker.new(current_user, oauth) - identity_linker.link + + link_identity(identity_linker) if identity_linker.changed? redirect_identity_linked @@ -100,6 +101,10 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController end end + def link_identity(identity_linker) + identity_linker.link + end + def redirect_identity_exists redirect_to after_sign_in_path_for(current_user) end diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb index 0e30df1b15b..62f98d9e549 100644 --- a/app/controllers/profiles/preferences_controller.rb +++ b/app/controllers/profiles/preferences_controller.rb @@ -44,7 +44,9 @@ class Profiles::PreferencesController < Profiles::ApplicationController :project_view, :theme_id, :first_day_of_week, - :preferred_language + :preferred_language, + :time_display_relative, + :time_format_in_24h ] end end diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index b9c52618d4b..1d16ddb1608 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -15,7 +15,7 @@ class ProfilesController < Profiles::ApplicationController result = Users::UpdateService.new(current_user, user_params.merge(user: @user)).execute if result[:status] == :success - message = "Profile was successfully updated" + message = s_("Profiles|Profile was successfully updated") format.html { redirect_back_or_default(default: { action: 'show' }, options: { notice: message }) } format.json { render json: { message: message } } @@ -31,7 +31,7 @@ class ProfilesController < Profiles::ApplicationController user.reset_incoming_email_token! end - flash[:notice] = "Incoming email token was successfully reset" + flash[:notice] = s_("Profiles|Incoming email token was successfully reset") redirect_to profile_personal_access_tokens_path end @@ -41,7 +41,7 @@ class ProfilesController < Profiles::ApplicationController user.reset_feed_token! end - flash[:notice] = 'Feed token was successfully reset' + flash[:notice] = s_('Profiles|Feed token was successfully reset') redirect_to profile_personal_access_tokens_path end @@ -106,6 +106,7 @@ class ProfilesController < Profiles::ApplicationController :organization, :private_profile, :include_private_contributions, + :timezone, status: [:emoji, :message] ) end diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 781eac7f080..80e4f54bbf4 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -3,7 +3,6 @@ class Projects::ApplicationController < ApplicationController include CookiesHelper include RoutableActions - include ProjectUnauthorized include ChecksCollaboration skip_before_action :authenticate_user! @@ -22,7 +21,7 @@ class Projects::ApplicationController < ApplicationController path = File.join(params[:namespace_id], params[:project_id] || params[:id]) auth_proc = ->(project) { !project.pending_delete? } - @project = find_routable!(Project, path, extra_authorization_proc: auth_proc, not_found_or_authorized_proc: project_unauthorized_proc) + @project = find_routable!(Project, path, extra_authorization_proc: auth_proc) end def build_canonical_path(project) diff --git a/app/controllers/projects/clusters/applications_controller.rb b/app/controllers/projects/clusters/applications_controller.rb index c7b6218d007..2a04b007304 100644 --- a/app/controllers/projects/clusters/applications_controller.rb +++ b/app/controllers/projects/clusters/applications_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class Projects::Clusters::ApplicationsController < Clusters::ApplicationsController - include ProjectUnauthorized - prepend_before_action :project private @@ -12,6 +10,6 @@ class Projects::Clusters::ApplicationsController < Clusters::ApplicationsControl end def project - @project ||= find_routable!(Project, File.join(params[:namespace_id], params[:project_id]), not_found_or_authorized_proc: project_unauthorized_proc) + @project ||= find_routable!(Project, File.join(params[:namespace_id], params[:project_id])) end end diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index feda6deeaa6..cb02581da37 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class Projects::ClustersController < Clusters::ClustersController - include ProjectUnauthorized - prepend_before_action :project before_action :repository @@ -15,7 +13,7 @@ class Projects::ClustersController < Clusters::ClustersController end def project - @project ||= find_routable!(Project, File.join(params[:namespace_id], params[:project_id]), not_found_or_authorized_proc: project_unauthorized_proc) + @project ||= find_routable!(Project, File.join(params[:namespace_id], params[:project_id])) end def repository diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index d8812c023ca..c342e1c80b0 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -14,6 +14,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController push_frontend_feature_flag(:metrics_time_window) push_frontend_feature_flag(:environment_metrics_use_prometheus_endpoint) push_frontend_feature_flag(:environment_metrics_show_multiple_dashboards) + push_frontend_feature_flag(:grafana_dashboard_link) end def index @@ -220,7 +221,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController def metrics_params return unless Feature.enabled?(:metrics_time_window, project) - return unless params[:start].present? || params[:end].present? params.require([:start, :end]) end diff --git a/app/controllers/projects/serverless/functions_controller.rb b/app/controllers/projects/serverless/functions_controller.rb index 8c3d141c888..79030da64d3 100644 --- a/app/controllers/projects/serverless/functions_controller.rb +++ b/app/controllers/projects/serverless/functions_controller.rb @@ -3,8 +3,6 @@ module Projects module Serverless class FunctionsController < Projects::ApplicationController - include ProjectUnauthorized - before_action :authorize_read_cluster! def index diff --git a/app/controllers/projects/settings/operations_controller.rb b/app/controllers/projects/settings/operations_controller.rb index 5cfb0ac307d..b5c77e5bbf4 100644 --- a/app/controllers/projects/settings/operations_controller.rb +++ b/app/controllers/projects/settings/operations_controller.rb @@ -5,6 +5,10 @@ module Projects class OperationsController < Projects::ApplicationController before_action :authorize_update_environment! + before_action do + push_frontend_feature_flag(:grafana_dashboard_link) + end + helper_method :error_tracking_setting def show diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 0fa4677ced1..07b38371ab9 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -4,6 +4,7 @@ class RegistrationsController < Devise::RegistrationsController include Recaptcha::Verify include AcceptsPendingInvitations + prepend_before_action :check_captcha, only: :create before_action :whitelist_query_limiting, only: [:destroy] before_action :ensure_terms_accepted, if: -> { Gitlab::CurrentSettings.current_application_settings.enforce_terms? }, @@ -21,15 +22,10 @@ class RegistrationsController < Devise::RegistrationsController params[resource_name] = params.delete(:"new_#{resource_name}") end - if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha - accept_pending_invitations - super do |new_user| - persist_accepted_terms_if_required(new_user) - end - else - flash[:alert] = s_('Profiles|There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.') - flash.delete :recaptcha_error - render action: 'new' + accept_pending_invitations + + super do |new_user| + persist_accepted_terms_if_required(new_user) end rescue Gitlab::Access::AccessDeniedError redirect_to(new_user_session_path) @@ -89,6 +85,17 @@ class RegistrationsController < Devise::RegistrationsController private + def check_captcha + return unless Feature.enabled?(:registrations_recaptcha, default_enabled: true) + return unless Gitlab::Recaptcha.load_configurations! + + return if verify_recaptcha + + flash[:alert] = _('There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.') + flash.delete :recaptcha_error + render action: 'new' + end + def sign_up_params params.require(:user).permit(:username, :email, :email_confirmation, :name, :password) end diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index 060b09f015c..5d28635232b 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -45,7 +45,7 @@ class UploadsController < ApplicationController when Appearance true else - permission = "read_#{model.class.to_s.underscore}".to_sym + permission = "read_#{model.class.underscore}".to_sym can?(current_user, permission, model) end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index f1dd040515f..52b6e828cfa 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -29,6 +29,7 @@ # updated_after: datetime # updated_before: datetime # attempt_group_search_optimizations: boolean +# attempt_project_search_optimizations: boolean # class IssuableFinder prepend FinderWithCrossProjectAccess @@ -184,7 +185,6 @@ class IssuableFinder @project = project end - # rubocop: disable CodeReuse/ActiveRecord def projects return @projects if defined?(@projects) @@ -192,17 +192,25 @@ class IssuableFinder projects = if current_user && params[:authorized_only].presence && !current_user_related? - current_user.authorized_projects + current_user.authorized_projects(min_access_level) elsif group - finder_options = { include_subgroups: params[:include_subgroups], only_owned: true } - GroupProjectsFinder.new(group: group, current_user: current_user, options: finder_options).execute # rubocop: disable CodeReuse/Finder + find_group_projects else - ProjectsFinder.new(current_user: current_user).execute # rubocop: disable CodeReuse/Finder + Project.public_or_visible_to_user(current_user, min_access_level) end - @projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil) + @projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil) # rubocop: disable CodeReuse/ActiveRecord + end + + def find_group_projects + return Project.none unless group + + if params[:include_subgroups] + Project.where(namespace_id: group.self_and_descendants) # rubocop: disable CodeReuse/ActiveRecord + else + group.projects + end.public_or_visible_to_user(current_user, min_access_level) end - # rubocop: enable CodeReuse/ActiveRecord def search params[:search].presence @@ -570,4 +578,8 @@ class IssuableFinder scope = params[:scope] scope == 'created_by_me' || scope == 'authored' || scope == 'assigned_to_me' end + + def min_access_level + ProjectFeature.required_minimum_access_level(klass) + end end diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index e6a82f55856..58a01d598ba 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -48,9 +48,9 @@ class IssuesFinder < IssuableFinder OR (issues.confidential = TRUE AND (issues.author_id = :user_id OR EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = :user_id AND issue_id = issues.id) - OR issues.project_id IN(:project_ids)))', + OR EXISTS (:authorizations)))', user_id: current_user.id, - project_ids: current_user.authorized_projects(CONFIDENTIAL_ACCESS_LEVEL).select(:id)) + authorizations: current_user.authorizations_for_projects(min_access_level: CONFIDENTIAL_ACCESS_LEVEL, related_project_column: "issues.project_id")) end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index 93d3c991846..23b731b1aed 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -62,7 +62,7 @@ class ProjectsFinder < UnionFinder collection = by_personal(collection) collection = by_starred(collection) collection = by_trending(collection) - collection = by_visibilty_level(collection) + collection = by_visibility_level(collection) collection = by_tags(collection) collection = by_search(collection) collection = by_archived(collection) @@ -71,12 +71,11 @@ class ProjectsFinder < UnionFinder collection end - # rubocop: disable CodeReuse/ActiveRecord def collection_with_user if owned_projects? current_user.owned_projects elsif min_access_level? - current_user.authorized_projects.where('project_authorizations.access_level >= ?', params[:min_access_level]) + current_user.authorized_projects(params[:min_access_level]) else if private_only? current_user.authorized_projects @@ -85,7 +84,6 @@ class ProjectsFinder < UnionFinder end end end - # rubocop: enable CodeReuse/ActiveRecord # Builds a collection for an anonymous user. def collection_without_user @@ -131,7 +129,7 @@ class ProjectsFinder < UnionFinder end # rubocop: disable CodeReuse/ActiveRecord - def by_visibilty_level(items) + def by_visibility_level(items) params[:visibility_level].present? ? items.where(visibility_level: params[:visibility_level]) : items end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb index a12568d5d31..897e12c1b56 100644 --- a/app/graphql/gitlab_schema.rb +++ b/app/graphql/gitlab_schema.rb @@ -7,6 +7,9 @@ class GitlabSchema < GraphQL::Schema AUTHENTICATED_COMPLEXITY = 250 ADMIN_COMPLEXITY = 300 + ANONYMOUS_MAX_DEPTH = 10 + AUTHENTICATED_MAX_DEPTH = 15 + use BatchLoader::GraphQL use Gitlab::Graphql::Authorize use Gitlab::Graphql::Present @@ -23,21 +26,36 @@ class GitlabSchema < GraphQL::Schema mutation(Types::MutationType) - def self.execute(query_str = nil, **kwargs) - kwargs[:max_complexity] ||= max_query_complexity(kwargs[:context]) + class << self + def execute(query_str = nil, **kwargs) + kwargs[:max_complexity] ||= max_query_complexity(kwargs[:context]) + kwargs[:max_depth] ||= max_query_depth(kwargs[:context]) - super(query_str, **kwargs) - end + super(query_str, **kwargs) + end + + private + + def max_query_complexity(ctx) + current_user = ctx&.fetch(:current_user, nil) + + if current_user&.admin + ADMIN_COMPLEXITY + elsif current_user + AUTHENTICATED_COMPLEXITY + else + DEFAULT_MAX_COMPLEXITY + end + end - def self.max_query_complexity(ctx) - current_user = ctx&.fetch(:current_user, nil) + def max_query_depth(ctx) + current_user = ctx&.fetch(:current_user, nil) - if current_user&.admin - ADMIN_COMPLEXITY - elsif current_user - AUTHENTICATED_COMPLEXITY - else - DEFAULT_MAX_COMPLEXITY + if current_user + AUTHENTICATED_MAX_DEPTH + else + ANONYMOUS_MAX_DEPTH + end end end end diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb index 1c3c24ad6dc..f7e49166ca0 100644 --- a/app/graphql/resolvers/issues_resolver.rb +++ b/app/graphql/resolvers/issues_resolver.rb @@ -46,7 +46,7 @@ module Resolvers def resolve(**args) # The project could have been loaded in batch by `BatchLoader`. # At this point we need the `id` of the project to query for issues, so - # make sure it's loaded and not `nil` before continueing. + # make sure it's loaded and not `nil` before continuing. project.sync if project.respond_to?(:sync) return Issue.none if project.nil? diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 5995ef57e26..971d1052824 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -286,4 +286,8 @@ module ApplicationSettingsHelper def expanded_by_default? Rails.env.test? end + + def instance_clusters_enabled? + can?(current_user, :read_cluster, Clusters::Instance.new) + end end diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb index d90ef8903a7..42732eb93dd 100644 --- a/app/helpers/dashboard_helper.rb +++ b/app/helpers/dashboard_helper.rb @@ -21,6 +21,10 @@ module DashboardHelper links.any? { |link| dashboard_nav_link?(link) } end + def has_start_trial? + false + end + private def get_dashboard_nav_links diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index 96471d15aac..dc0e5511fcf 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -91,6 +91,28 @@ module EmailsHelper ].join(';') end + def closure_reason_text(closed_via, format: nil) + case closed_via + when MergeRequest + merge_request = MergeRequest.find(closed_via[:id]).present + + case format + when :html + " via merge request #{link_to(merge_request.to_reference, merge_request.web_url)}" + else + # If it's not HTML nor text then assume it's text to be safe + " via merge request #{merge_request.to_reference} (#{merge_request.web_url})" + end + when String + # Technically speaking this should be Commit but per + # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15610#note_163812339 + # we can't deserialize Commit without custom serializer for ActiveJob + " via #{closed_via}" + else + "" + end + end + # "You are receiving this email because #{reason}" def notification_reason_text(reason) string = case reason diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 1371e9993b4..e990e425cb6 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -68,7 +68,7 @@ module EventsHelper end def event_preposition(event) - if event.push? || event.commented? || event.target + if event.push_action? || event.commented_action? || event.target "at" elsif event.milestone? "in" @@ -80,11 +80,11 @@ module EventsHelper words << event.author_name words << event_action_name(event) - if event.push? + if event.push_action? words << event.ref_type words << event.ref_name words << "at" - elsif event.commented? + elsif event.commented_action? words << event.note_target_reference words << "at" elsif event.milestone? @@ -121,9 +121,9 @@ module EventsHelper if event.note_target event_note_target_url(event) end - elsif event.push? + elsif event.push_action? push_event_feed_url(event) - elsif event.created_project? + elsif event.created_project_action? project_url(event.project) end end @@ -147,7 +147,7 @@ module EventsHelper def event_feed_summary(event) if event.issue? render "events/event_issue", issue: event.issue - elsif event.push? + elsif event.push_action? render "events/event_push", event: event elsif event.merge_request? render "events/event_merge_request", merge_request: event.merge_request diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 05da5ebdb22..a57ba5f3a4f 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -58,6 +58,14 @@ module NavHelper current_path?('milestones#show') end + def admin_monitoring_nav_links + %w(system_info background_jobs logs health_check requests_profiles) + end + + def group_issues_sub_menu_items + %w(groups#issues labels#index milestones#index boards#index boards#show) + end + private def get_header_links diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 8977ccaa9d8..91d15e0e4ea 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -239,8 +239,10 @@ module ProjectsHelper end # rubocop: enable CodeReuse/ActiveRecord + # TODO: Remove this method when removing the feature flag + # https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/11209#note_162234863 def show_projects?(projects, params) - !!(params[:personal] || params[:name] || any_projects?(projects)) + Feature.enabled?(:project_list_filter_bar) || !!(params[:personal] || params[:name] || any_projects?(projects)) end def push_to_create_project_command(user = current_user) @@ -313,6 +315,10 @@ module ProjectsHelper ) % { default_label: default_label } end + def can_import_members? + Ability.allowed?(current_user, :admin_project_member, @project) + end + private def get_project_nav_tabs(project, current_user) diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index a62c00df60b..4594f5a31b9 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -128,7 +128,7 @@ module SearchHelper # rubocop: disable CodeReuse/ActiveRecord def projects_autocomplete(term, limit = 5) current_user.authorized_projects.order_id_desc.search_by_title(term) - .sorted_by_stars.non_archived.limit(limit).map do |p| + .sorted_by_stars_desc.non_archived.limit(limit).map do |p| { category: "Projects", id: p.id, diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index 6524ba55a16..f2d814e6930 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -30,13 +30,20 @@ module SortingHelper end def projects_sort_options_hash + Feature.enabled?(:project_list_filter_bar) && !current_controller?('admin/projects') ? projects_sort_common_options_hash : old_projects_sort_options_hash + end + + # TODO: Simplify these sorting options + # https://gitlab.com/gitlab-org/gitlab-ce/issues/60798 + # https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/11209#note_162234858 + def old_projects_sort_options_hash options = { sort_value_latest_activity => sort_title_latest_activity, sort_value_name => sort_title_name, sort_value_oldest_activity => sort_title_oldest_activity, sort_value_oldest_created => sort_title_oldest_created, sort_value_recently_created => sort_title_recently_created, - sort_value_most_stars => sort_title_most_stars + sort_value_stars_desc => sort_title_most_stars } if current_controller?('admin/projects') @@ -46,6 +53,41 @@ module SortingHelper options end + def projects_sort_common_options_hash + { + sort_value_latest_activity => sort_title_latest_activity, + sort_value_recently_created => sort_title_created_date, + sort_value_name => sort_title_name, + sort_value_stars_desc => sort_title_stars + } + end + + def projects_sort_option_titles + { + sort_value_latest_activity => sort_title_latest_activity, + sort_value_recently_created => sort_title_created_date, + sort_value_name => sort_title_name, + sort_value_stars_desc => sort_title_stars, + sort_value_oldest_activity => sort_title_latest_activity, + sort_value_oldest_created => sort_title_created_date, + sort_value_name_desc => sort_title_name, + sort_value_stars_asc => sort_title_stars + } + end + + def projects_reverse_sort_options_hash + { + sort_value_latest_activity => sort_value_oldest_activity, + sort_value_recently_created => sort_value_oldest_created, + sort_value_name => sort_value_name_desc, + sort_value_stars_desc => sort_value_stars_asc, + sort_value_oldest_activity => sort_value_latest_activity, + sort_value_oldest_created => sort_value_recently_created, + sort_value_name_desc => sort_value_name, + sort_value_stars_asc => sort_value_stars_desc + } + end + def groups_sort_options_hash { sort_value_name => sort_title_name, @@ -59,7 +101,7 @@ module SortingHelper def subgroups_sort_options_hash groups_sort_options_hash.merge( - sort_value_most_stars => sort_title_most_stars + sort_value_stars_desc => sort_title_most_stars ) end @@ -176,6 +218,8 @@ module SortingHelper end end + # TODO: dedupicate issuable and project sort direction + # https://gitlab.com/gitlab-org/gitlab-ce/issues/60798 def issuable_sort_direction_button(sort_value) link_class = 'btn btn-default has-tooltip reverse-sort-btn qa-reverse-sort' reverse_sort = issuable_reverse_sort_order_hash[sort_value] @@ -187,7 +231,23 @@ module SortingHelper link_class += ' disabled' end - link_to(reverse_url, type: 'button', class: link_class, title: 'Sort direction') do + link_to(reverse_url, type: 'button', class: link_class, title: s_('SortOptions|Sort direction')) do + sprite_icon("sort-#{issuable_sort_icon_suffix(sort_value)}", size: 16) + end + end + + def project_sort_direction_button(sort_value) + link_class = 'btn btn-default has-tooltip reverse-sort-btn qa-reverse-sort' + reverse_sort = projects_reverse_sort_options_hash[sort_value] + + if reverse_sort + reverse_url = filter_projects_path(sort: reverse_sort) + else + reverse_url = '#' + link_class += ' disabled' + end + + link_to(reverse_url, type: 'button', class: link_class, title: s_('SortOptions|Sort direction')) do sprite_icon("sort-#{issuable_sort_icon_suffix(sort_value)}", size: 16) end end @@ -325,6 +385,10 @@ module SortingHelper s_('SortOptions|Most stars') end + def sort_title_stars + s_('SortOptions|Stars') + end + def sort_title_oldest_last_activity s_('SortOptions|Oldest last activity') end @@ -466,10 +530,14 @@ module SortingHelper 'contacted_asc' end - def sort_value_most_stars + def sort_value_stars_desc 'stars_desc' end + def sort_value_stars_asc + 'stars_asc' + end + def sort_value_oldest_last_activity 'last_activity_on_asc' end diff --git a/app/helpers/storage_helper.rb b/app/helpers/storage_helper.rb index 15041bd5805..e80b3f2b54a 100644 --- a/app/helpers/storage_helper.rb +++ b/app/helpers/storage_helper.rb @@ -2,6 +2,8 @@ module StorageHelper def storage_counter(size_in_bytes) + return s_('StorageSize|Unknown') unless size_in_bytes + precision = size_in_bytes < 1.megabyte ? 0 : 1 number_to_human_size(size_in_bytes, delimiter: ',', precision: precision, significant: false) diff --git a/app/mailers/devise_mailer.rb b/app/mailers/devise_mailer.rb index 7aa75ee30e6..cbaf53fced1 100644 --- a/app/mailers/devise_mailer.rb +++ b/app/mailers/devise_mailer.rb @@ -7,6 +7,7 @@ class DeviseMailer < Devise::Mailer layout 'mailer/devise' helper EmailsHelper + helper ApplicationHelper protected diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index d2e334fb856..2b046d17122 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -30,8 +30,8 @@ module Emails end # rubocop: enable CodeReuse/ActiveRecord - def closed_issue_email(recipient_id, issue_id, updated_by_user_id, reason = nil) - setup_issue_mail(issue_id, recipient_id) + def closed_issue_email(recipient_id, issue_id, updated_by_user_id, reason: nil, closed_via: nil) + setup_issue_mail(issue_id, recipient_id, closed_via: closed_via) @updated_by = User.find(updated_by_user_id) mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason)) @@ -91,10 +91,11 @@ module Emails private - def setup_issue_mail(issue_id, recipient_id) + def setup_issue_mail(issue_id, recipient_id, closed_via: nil) @issue = Issue.find(issue_id) @project = @issue.project @target_url = project_issue_url(@project, @issue) + @closed_via = closed_via @sent_notification = SentNotification.record(@issue, recipient_id, reply_key) end diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb index 63148831a24..fc6ed695675 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -58,14 +58,14 @@ module Emails })) end - def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason = nil) + def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason: nil, closed_via: nil) setup_merge_request_mail(merge_request_id, recipient_id) @updated_by = User.find(updated_by_user_id) mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason)) end - def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason = nil) + def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason: nil, closed_via: nil) setup_merge_request_mail(merge_request_id, recipient_id) mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason)) diff --git a/app/models/active_session.rb b/app/models/active_session.rb index 1e01f1d17e6..f355b02c428 100644 --- a/app/models/active_session.rb +++ b/app/models/active_session.rb @@ -53,7 +53,7 @@ class ActiveSession def self.list(user) Gitlab::Redis::SharedState.with do |redis| - cleaned_up_lookup_entries(redis, user.id).map do |entry| + cleaned_up_lookup_entries(redis, user).map do |entry| # rubocop:disable Security/MarshalLoad Marshal.load(entry) # rubocop:enable Security/MarshalLoad @@ -78,7 +78,7 @@ class ActiveSession def self.cleanup(user) Gitlab::Redis::SharedState.with do |redis| - cleaned_up_lookup_entries(redis, user.id) + cleaned_up_lookup_entries(redis, user) end end @@ -90,25 +90,52 @@ class ActiveSession "#{Gitlab::Redis::SharedState::USER_SESSIONS_LOOKUP_NAMESPACE}:#{user_id}" end - def self.cleaned_up_lookup_entries(redis, user_id) - lookup_key = lookup_key_name(user_id) + def self.list_sessions(user) + sessions_from_ids(session_ids_for_user(user)) + end - session_ids = redis.smembers(lookup_key) + def self.session_ids_for_user(user) + Gitlab::Redis::SharedState.with do |redis| + redis.smembers(lookup_key_name(user.id)) + end + end - entry_keys = session_ids.map { |session_id| key_name(user_id, session_id) } - return [] if entry_keys.empty? + def self.sessions_from_ids(session_ids) + return [] if session_ids.empty? - entries = redis.mget(entry_keys) + Gitlab::Redis::SharedState.with do |redis| + session_keys = session_ids.map { |session_id| "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}" } - session_ids_and_entries = session_ids.zip(entries) + redis.mget(session_keys).compact.map do |raw_session| + # rubocop:disable Security/MarshalLoad + Marshal.load(raw_session) + # rubocop:enable Security/MarshalLoad + end + end + end + + def self.raw_active_session_entries(session_ids, user_id) + return [] if session_ids.empty? + + Gitlab::Redis::SharedState.with do |redis| + entry_keys = session_ids.map { |session_id| key_name(user_id, session_id) } + + redis.mget(entry_keys) + end + end + + def self.cleaned_up_lookup_entries(redis, user) + session_ids = session_ids_for_user(user) + entries = raw_active_session_entries(session_ids, user.id) # remove expired keys. # only the single key entries are automatically expired by redis, the # lookup entries in the set need to be removed manually. + session_ids_and_entries = session_ids.zip(entries) session_ids_and_entries.reject { |_session_id, entry| entry }.each do |session_id, _entry| - redis.srem(lookup_key, session_id) + redis.srem(lookup_key_name(user.id), session_id) end - session_ids_and_entries.select { |_session_id, entry| entry }.map { |_session_id, entry| entry } + entries.compact end end diff --git a/app/models/application_record.rb b/app/models/application_record.rb index d1d01368972..0979d03f6e6 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -41,4 +41,8 @@ class ApplicationRecord < ActiveRecord::Base find_or_create_by(*args) end end + + def self.underscore + Gitlab::SafeRequestStore.fetch("model:#{self}:underscore") { self.to_s.underscore } + end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index d2f5ff13408..1b67a7272bc 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -140,6 +140,8 @@ module Ci where("EXISTS (?)", matcher) end + scope :queued_before, ->(time) { where(arel_table[:queued_at].lt(time)) } + ## # TODO: Remove these mounters when we remove :ci_enable_legacy_artifacts feature flag mount_uploader :legacy_artifacts_file, LegacyArtifactUploader, mount_on: :artifacts_file @@ -378,8 +380,6 @@ module Ci end def any_unmet_prerequisites? - return false unless Feature.enabled?(:ci_preparing_state, default_enabled: true) - prerequisites.present? end diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index f9cf398556d..3beb76ffc2b 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -99,7 +99,7 @@ module Ci raw: 1, zip: 2, gzip: 3 - } + }, _suffix: true # `file_location` indicates where actual files are stored. # Ideally, actual files should be stored in the same directory, and use the same diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb index 1454b2dfb39..c0a0ca9acf6 100644 --- a/app/models/ci/pipeline_schedule.rb +++ b/app/models/ci/pipeline_schedule.rb @@ -5,6 +5,7 @@ module Ci extend Gitlab::Ci::Model include Importable include IgnorableColumn + include StripAttribute ignore_column :deleted_at @@ -22,6 +23,8 @@ module Ci before_save :set_next_run_at + strip_attributes :cron + scope :active, -> { where(active: true) } scope :inactive, -> { where(active: false) } diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb index 987c057ad6d..36c51522089 100644 --- a/app/models/clusters/applications/jupyter.rb +++ b/app/models/clusters/applications/jupyter.rb @@ -39,7 +39,7 @@ module Clusters end # Will be addressed in future MRs - # We need to investigate and document what will be permenantly deleted. + # We need to investigate and document what will be permanently deleted. def allowed_to_uninstall? false end diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index af648db3708..ceecd931bba 100644 --- a/app/models/clusters/applications/runner.rb +++ b/app/models/clusters/applications/runner.rb @@ -69,10 +69,12 @@ module Clusters } if cluster.group_type? - attributes.merge(groups: [group]) + attributes[:groups] = [group] elsif cluster.project_type? - attributes.merge(projects: [project]) + attributes[:projects] = [project] end + + attributes end def gitlab_url diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index d2b1adacbfb..9299e61dad3 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -115,10 +115,12 @@ module Clusters } def self.ancestor_clusters_for_clusterable(clusterable, hierarchy_order: :asc) + return [] if clusterable.is_a?(Instance) + hierarchy_groups = clusterable.ancestors_upto(hierarchy_order: hierarchy_order).eager_load(:clusters) hierarchy_groups = hierarchy_groups.merge(current_scope) if current_scope - hierarchy_groups.flat_map(&:clusters) + hierarchy_groups.flat_map(&:clusters) + Instance.new.clusters end def status_name @@ -177,6 +179,10 @@ module Clusters end alias_method :group, :first_group + def instance + Instance.new if instance_type? + end + def kubeclient platform_kubernetes.kubeclient if kubernetes? end diff --git a/app/models/clusters/concerns/application_data.rb b/app/models/clusters/concerns/application_data.rb index a48ee340fac..3479fea415e 100644 --- a/app/models/clusters/concerns/application_data.rb +++ b/app/models/clusters/concerns/application_data.rb @@ -3,56 +3,52 @@ module Clusters module Concerns module ApplicationData - extend ActiveSupport::Concern - - included do - def uninstall_command - Gitlab::Kubernetes::Helm::DeleteCommand.new( - name: name, - rbac: cluster.platform_kubernetes_rbac?, - files: files - ) - end + def uninstall_command + Gitlab::Kubernetes::Helm::DeleteCommand.new( + name: name, + rbac: cluster.platform_kubernetes_rbac?, + files: files + ) + end - def repository - nil - end + def repository + nil + end - def values - File.read(chart_values_file) - end + def values + File.read(chart_values_file) + end - def files - @files ||= begin - files = { 'values.yaml': values } + def files + @files ||= begin + files = { 'values.yaml': values } - files.merge!(certificate_files) if cluster.application_helm.has_ssl? + files.merge!(certificate_files) if cluster.application_helm.has_ssl? - files - end + files end + end - private + private - def certificate_files - { - 'ca.pem': ca_cert, - 'cert.pem': helm_cert.cert_string, - 'key.pem': helm_cert.key_string - } - end + def certificate_files + { + 'ca.pem': ca_cert, + 'cert.pem': helm_cert.cert_string, + 'key.pem': helm_cert.key_string + } + end - def ca_cert - cluster.application_helm.ca_cert - end + def ca_cert + cluster.application_helm.ca_cert + end - def helm_cert - @helm_cert ||= cluster.application_helm.issue_client_cert - end + def helm_cert + @helm_cert ||= cluster.application_helm.issue_client_cert + end - def chart_values_file - "#{Rails.root}/vendor/#{name}/values.yaml" - end + def chart_values_file + "#{Rails.root}/vendor/#{name}/values.yaml" end end end diff --git a/app/models/clusters/instance.rb b/app/models/clusters/instance.rb new file mode 100644 index 00000000000..d8a888d53ba --- /dev/null +++ b/app/models/clusters/instance.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Clusters + class Instance + def clusters + Clusters::Cluster.instance_type + end + + def feature_available?(feature) + ::Feature.enabled?(feature, default_enabled: true) + end + + def self.enabled? + ::Feature.enabled?(:instance_clusters, default_enabled: true) + end + end +end diff --git a/app/models/concerns/deployment_platform.rb b/app/models/concerns/deployment_platform.rb index 0107af5f8ec..9ac0d612db3 100644 --- a/app/models/concerns/deployment_platform.rb +++ b/app/models/concerns/deployment_platform.rb @@ -14,6 +14,7 @@ module DeploymentPlatform def find_deployment_platform(environment) find_cluster_platform_kubernetes(environment: environment) || find_group_cluster_platform_kubernetes_with_feature_guard(environment: environment) || + find_instance_cluster_platform_kubernetes_with_feature_guard(environment: environment) || find_kubernetes_service_integration || build_cluster_and_deployment_platform end @@ -36,6 +37,18 @@ module DeploymentPlatform .first&.platform_kubernetes end + def find_instance_cluster_platform_kubernetes_with_feature_guard(environment: nil) + return unless Clusters::Instance.enabled? + + find_instance_cluster_platform_kubernetes(environment: environment) + end + + # EE would override this and utilize environment argument + def find_instance_cluster_platform_kubernetes(environment: nil) + Clusters::Instance.new.clusters.enabled.default_environment + .first&.platform_kubernetes + end + def find_kubernetes_service_integration services.deployment.reorder(nil).find_by(active: true) end diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index 8882f48c281..78bcce2f592 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -66,6 +66,10 @@ module HasStatus def all_state_names state_machines.values.flat_map(&:states).flat_map { |s| s.map(&:name) } end + + def completed_statuses + COMPLETED_STATUSES.map(&:to_sym) + end end included do diff --git a/app/models/environment.rb b/app/models/environment.rb index 0eda7a2513f..aff20dae09b 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -155,11 +155,11 @@ class Environment < ApplicationRecord end def has_terminals? - project.deployment_platform.present? && available? && last_deployment.present? + deployment_platform.present? && available? && last_deployment.present? end def terminals - project.deployment_platform.terminals(self) if has_terminals? + deployment_platform.terminals(self) if has_terminals? end def has_metrics? diff --git a/app/models/event.rb b/app/models/event.rb index 593acf5edfe..738080eb584 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -68,7 +68,7 @@ class Event < ApplicationRecord # Callbacks after_create :reset_project_activity - after_create :set_last_repository_updated_at, if: :push? + after_create :set_last_repository_updated_at, if: :push_action? after_create :track_user_interacted_projects # Scopes @@ -138,11 +138,11 @@ class Event < ApplicationRecord # rubocop:disable Metrics/CyclomaticComplexity # rubocop:disable Metrics/PerceivedComplexity def visible_to_user?(user = nil) - if push? || commit_note? + if push_action? || commit_note? Ability.allowed?(user, :download_code, project) elsif membership_changed? Ability.allowed?(user, :read_project, project) - elsif created_project? + elsif created_project_action? Ability.allowed?(user, :read_project, project) elsif issue? || issue_note? Ability.allowed?(user, :read_issue, note? ? note_target : target) @@ -173,56 +173,56 @@ class Event < ApplicationRecord target.try(:title) end - def created? + def created_action? action == CREATED end - def push? + def push_action? false end - def merged? + def merged_action? action == MERGED end - def closed? + def closed_action? action == CLOSED end - def reopened? + def reopened_action? action == REOPENED end - def joined? + def joined_action? action == JOINED end - def left? + def left_action? action == LEFT end - def expired? + def expired_action? action == EXPIRED end - def destroyed? + def destroyed_action? action == DESTROYED end - def commented? + def commented_action? action == COMMENTED end def membership_changed? - joined? || left? || expired? + joined_action? || left_action? || expired_action? end - def created_project? - created? && !target && target_type.nil? + def created_project_action? + created_action? && !target && target_type.nil? end def created_target? - created? && target + created_action? && target end def milestone? @@ -258,23 +258,23 @@ class Event < ApplicationRecord end def action_name - if push? + if push_action? push_action_name - elsif closed? + elsif closed_action? "closed" - elsif merged? + elsif merged_action? "accepted" - elsif joined? + elsif joined_action? 'joined' - elsif left? + elsif left_action? 'left' - elsif expired? + elsif expired_action? 'removed due to membership expiration from' - elsif destroyed? + elsif destroyed_action? 'destroyed' - elsif commented? + elsif commented_action? "commented on" - elsif created_project? + elsif created_project_action? created_project_action_name else "opened" @@ -337,7 +337,7 @@ class Event < ApplicationRecord end def body? - if push? + if push_action? push_with_commits? elsif note? true diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index b266c61f002..4cba69069bb 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -12,7 +12,7 @@ class GroupMember < Member validates :source_type, format: { with: /\ANamespace\z/ } default_scope { where(source_type: SOURCE_TYPE) } - scope :in_groups, ->(groups) { where(source_id: groups.select(:id)) } + scope :of_groups, ->(groups) { where(source_id: groups.select(:id)) } scope :count_users_by_group_id, -> { joins(:user).group(:source_id).count } diff --git a/app/models/project.rb b/app/models/project.rb index 228ab9e9618..ab4da61dcf8 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -56,7 +56,6 @@ class Project < ApplicationRecord VALID_MIRROR_PROTOCOLS = %w(http https ssh git).freeze ignore_column :import_status, :import_jid, :import_error - ignore_column :ci_id cache_markdown_field :description, pipeline: :description @@ -337,8 +336,8 @@ class Project < ApplicationRecord validates :star_count, numericality: { greater_than_or_equal_to: 0 } validate :check_personal_projects_limit, on: :create validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? } - validate :visibility_level_allowed_by_group, if: -> { changes.has_key?(:visibility_level) } - validate :visibility_level_allowed_as_fork, if: -> { changes.has_key?(:visibility_level) } + validate :visibility_level_allowed_by_group, if: :should_validate_visibility_level? + validate :visibility_level_allowed_as_fork, if: :should_validate_visibility_level? validate :check_wiki_path_conflict validate :validate_pages_https_only, if: -> { changes.has_key?(:pages_https_only) } validates :repository_storage, @@ -357,7 +356,8 @@ class Project < ApplicationRecord # last_activity_at is throttled every minute, but last_repository_updated_at is updated with every push scope :sorted_by_activity, -> { reorder("GREATEST(COALESCE(last_activity_at, '1970-01-01'), COALESCE(last_repository_updated_at, '1970-01-01')) DESC") } - scope :sorted_by_stars, -> { reorder(star_count: :desc) } + scope :sorted_by_stars_desc, -> { reorder(star_count: :desc) } + scope :sorted_by_stars_asc, -> { reorder(star_count: :asc) } scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) } scope :personal, ->(user) { where(namespace_id: user.namespace_id) } @@ -463,10 +463,12 @@ class Project < ApplicationRecord # Returns a collection of projects that is either public or visible to the # logged in user. - def self.public_or_visible_to_user(user = nil) + def self.public_or_visible_to_user(user = nil, min_access_level = nil) + min_access_level = nil if user&.admin? + if user where('EXISTS (?) OR projects.visibility_level IN (?)', - user.authorizations_for_projects, + user.authorizations_for_projects(min_access_level: min_access_level), Gitlab::VisibilityLevel.levels_for_user(user)) else public_to_user @@ -476,30 +478,32 @@ class Project < ApplicationRecord # project features may be "disabled", "internal", "enabled" or "public". If "internal", # they are only available to team members. This scope returns projects where # the feature is either public, enabled, or internal with permission for the user. + # Note: this scope doesn't enforce that the user has access to the projects, it just checks + # that the user has access to the feature. It's important to use this scope with others + # that checks project authorizations first. # # This method uses an optimised version of `with_feature_access_level` for # logged in users to more efficiently get private projects with the given # feature. def self.with_feature_available_for_user(feature, user) visible = [ProjectFeature::ENABLED, ProjectFeature::PUBLIC] - min_access_level = ProjectFeature.required_minimum_access_level(feature) if user&.admin? with_feature_enabled(feature) elsif user + min_access_level = ProjectFeature.required_minimum_access_level(feature) column = ProjectFeature.quoted_access_level_column(feature) with_project_feature - .where( - "(projects.visibility_level > :private AND (#{column} IS NULL OR #{column} >= (:public_visible) OR (#{column} = :private_visible AND EXISTS(:authorizations))))"\ - " OR (projects.visibility_level = :private AND (#{column} IS NULL OR #{column} >= :private_visible) AND EXISTS(:authorizations))", - { - private: Gitlab::VisibilityLevel::PRIVATE, - public_visible: ProjectFeature::ENABLED, - private_visible: ProjectFeature::PRIVATE, - authorizations: user.authorizations_for_projects(min_access_level: min_access_level) - }) + .where("#{column} IS NULL OR #{column} IN (:public_visible) OR (#{column} = :private_visible AND EXISTS (:authorizations))", + { + public_visible: visible, + private_visible: ProjectFeature::PRIVATE, + authorizations: user.authorizations_for_projects(min_access_level: min_access_level) + }) else + # This has to be added to include features whose value is nil in the db + visible << nil with_feature_access_level(feature, visible) end end @@ -544,7 +548,9 @@ class Project < ApplicationRecord when 'latest_activity_asc' reorder(last_activity_at: :asc) when 'stars_desc' - sorted_by_stars + sorted_by_stars_desc + when 'stars_asc' + sorted_by_stars_asc else order_by(method) end @@ -885,6 +891,10 @@ class Project < ApplicationRecord self.errors.add(:limit_reached, error % { limit: limit }) end + def should_validate_visibility_level? + new_record? || changes.has_key?(:visibility_level) + end + def visibility_level_allowed_by_group return if visibility_level_allowed_by_group? diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index ebf28dc842c..7b4832b84a8 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -265,6 +265,7 @@ class JiraService < IssueTrackerService def find_remote_link(issue, url) links = jira_request { issue.remotelink.all } + return unless links links.find { |link| link.object["url"] == url } end diff --git a/app/models/push_event.rb b/app/models/push_event.rb index 9c0267c3140..4698df39730 100644 --- a/app/models/push_event.rb +++ b/app/models/push_event.rb @@ -69,7 +69,7 @@ class PushEvent < Event PUSHED end - def push? + def push_action? true end diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb index cbfc1a7c1b2..af705b29f7a 100644 --- a/app/models/remote_mirror.rb +++ b/app/models/remote_mirror.rb @@ -133,6 +133,10 @@ class RemoteMirror < ApplicationRecord end alias_method :enabled?, :enabled + def disabled? + !enabled? + end + def updated_since?(timestamp) last_update_started_at && last_update_started_at > timestamp && !update_failed? end diff --git a/app/models/repository.rb b/app/models/repository.rb index be17b54ff12..e05d3dd58ac 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -283,14 +283,19 @@ class Repository end def diverging_commit_counts(branch) + return diverging_commit_counts_without_max(branch) if Feature.enabled?('gitaly_count_diverging_commits_no_max') + + ## TODO: deprecate the below code after 12.0 @root_ref_hash ||= raw_repository.commit(root_ref).id cache.fetch(:"diverging_commit_counts_#{branch.name}") do # Rugged seems to throw a `ReferenceError` when given branch_names rather # than SHA-1 hashes + branch_sha = branch.dereferenced_target.sha + number_commits_behind, number_commits_ahead = raw_repository.diverging_commit_count( @root_ref_hash, - branch.dereferenced_target.sha, + branch_sha, max_count: MAX_DIVERGING_COUNT) if number_commits_behind + number_commits_ahead >= MAX_DIVERGING_COUNT @@ -301,6 +306,22 @@ class Repository end end + def diverging_commit_counts_without_max(branch) + @root_ref_hash ||= raw_repository.commit(root_ref).id + cache.fetch(:"diverging_commit_counts_without_max_#{branch.name}") do + # Rugged seems to throw a `ReferenceError` when given branch_names rather + # than SHA-1 hashes + branch_sha = branch.dereferenced_target.sha + + number_commits_behind, number_commits_ahead = + raw_repository.diverging_commit_count( + @root_ref_hash, + branch_sha) + + { behind: number_commits_behind, ahead: number_commits_ahead } + end + end + def archive_metadata(ref, storage_path, format = "tar.gz", append_sha:, path: nil) raw_repository.archive_metadata( ref, @@ -465,7 +486,7 @@ class Repository def after_import expire_content_cache - DetectRepositoryLanguagesWorker.perform_async(project.id, project.owner.id) + DetectRepositoryLanguagesWorker.perform_async(project.id) end # Runs code after a new commit has been pushed. @@ -1050,7 +1071,7 @@ class Repository # To support the full deprecated behaviour, set the # `rebase_commit_sha` for the merge_request here and return the value - merge_request.update(rebase_commit_sha: rebase_sha) + merge_request.update(rebase_commit_sha: rebase_sha, merge_error: nil) rebase_sha end @@ -1069,7 +1090,7 @@ class Repository remote_repository: merge_request.target_project.repository.raw, remote_branch: merge_request.target_branch ) do |commit_id| - merge_request.update!(rebase_commit_sha: commit_id) + merge_request.update!(rebase_commit_sha: commit_id, merge_error: nil) end end end diff --git a/app/models/user.rb b/app/models/user.rb index 43039f3760e..60f69659a6b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -230,6 +230,9 @@ class User < ApplicationRecord delegate :notes_filter_for, to: :user_preference delegate :set_notes_filter, to: :user_preference delegate :first_day_of_week, :first_day_of_week=, to: :user_preference + delegate :timezone, :timezone=, to: :user_preference + delegate :time_display_relative, :time_display_relative=, to: :user_preference + delegate :time_format_in_24h, :time_format_in_24h=, to: :user_preference accepts_nested_attributes_for :user_preference, update_only: true @@ -757,11 +760,15 @@ class User < ApplicationRecord # Typically used in conjunction with projects table to get projects # a user has been given access to. + # The param `related_project_column` is the column to compare to the + # project_authorizations. By default is projects.id # # Example use: # `Project.where('EXISTS(?)', user.authorizations_for_projects)` - def authorizations_for_projects(min_access_level: nil) - authorizations = project_authorizations.select(1).where('project_authorizations.project_id = projects.id') + def authorizations_for_projects(min_access_level: nil, related_project_column: 'projects.id') + authorizations = project_authorizations + .select(1) + .where("project_authorizations.project_id = #{related_project_column}") return authorizations unless min_access_level.present? diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index 282b192167f..f1326f4c8cb 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -10,6 +10,10 @@ class UserPreference < ApplicationRecord validates :issue_notes_filter, :merge_request_notes_filter, inclusion: { in: NOTES_FILTERS.values }, presence: true + default_value_for :timezone, value: Time.zone.tzinfo.name, allows_nil: false + default_value_for :time_display_relative, value: true, allows_nil: false + default_value_for :time_format_in_24h, value: false, allows_nil: false + class << self def notes_filters { diff --git a/app/policies/clusters/cluster_policy.rb b/app/policies/clusters/cluster_policy.rb index d6d590687e2..316bd39f7a3 100644 --- a/app/policies/clusters/cluster_policy.rb +++ b/app/policies/clusters/cluster_policy.rb @@ -6,5 +6,6 @@ module Clusters delegate { cluster.group } delegate { cluster.project } + delegate { cluster.instance } end end diff --git a/app/policies/clusters/instance_policy.rb b/app/policies/clusters/instance_policy.rb new file mode 100644 index 00000000000..e1045c85e6d --- /dev/null +++ b/app/policies/clusters/instance_policy.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Clusters + class InstancePolicy < BasePolicy + include ClusterableActions + + condition(:has_clusters, scope: :subject) { clusterable_has_clusters? } + condition(:can_have_multiple_clusters) { multiple_clusters_available? } + condition(:instance_clusters_enabled) { Instance.enabled? } + + rule { admin & instance_clusters_enabled }.policy do + enable :read_cluster + enable :add_cluster + enable :create_cluster + enable :update_cluster + enable :admin_cluster + end + + rule { ~can_have_multiple_clusters & has_clusters }.prevent :add_cluster + end +end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index eb2e536e8e9..ea86858181d 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -129,6 +129,10 @@ class GroupPolicy < BasePolicy def access_level return GroupMember::NO_ACCESS if @user.nil? - @access_level ||= @subject.max_member_access_for_user(@user) + @access_level ||= lookup_access_level! + end + + def lookup_access_level! + @subject.max_member_access_for_user(@user) end end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 76544249688..3218c04b219 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -488,6 +488,10 @@ class ProjectPolicy < BasePolicy def team_access_level return -1 if @user.nil? + lookup_access_level! + end + + def lookup_access_level! # NOTE: max_member_access has its own cache project.team.max_member_access(@user.id) end diff --git a/app/presenters/clusterable_presenter.rb b/app/presenters/clusterable_presenter.rb index a9edfc92177..34bdf156623 100644 --- a/app/presenters/clusterable_presenter.rb +++ b/app/presenters/clusterable_presenter.rb @@ -52,10 +52,6 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated raise NotImplementedError end - def clusters_path(params = {}) - raise NotImplementedError - end - def empty_state_help_text nil end diff --git a/app/presenters/clusters/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb index 81994bbce7d..33b217c8498 100644 --- a/app/presenters/clusters/cluster_presenter.rb +++ b/app/presenters/clusters/cluster_presenter.rb @@ -35,6 +35,8 @@ module Clusters s_("ClusterIntegration|Project cluster") elsif cluster.group_type? s_("ClusterIntegration|Group cluster") + elsif cluster.instance_type? + s_("ClusterIntegration|Instance cluster") end end @@ -43,6 +45,8 @@ module Clusters project_cluster_path(project, cluster) elsif cluster.group_type? group_cluster_path(group, cluster) + elsif cluster.instance_type? + admin_cluster_path(cluster) else raise NotImplementedError end diff --git a/app/presenters/group_clusterable_presenter.rb b/app/presenters/group_clusterable_presenter.rb index 15db3aabafe..f5b0bb64487 100644 --- a/app/presenters/group_clusterable_presenter.rb +++ b/app/presenters/group_clusterable_presenter.rb @@ -24,11 +24,6 @@ class GroupClusterablePresenter < ClusterablePresenter group_cluster_path(clusterable, cluster, params) end - override :clusters_path - def clusters_path(params = {}) - group_clusters_path(clusterable, params) - end - override :empty_state_help_text def empty_state_help_text s_('ClusterIntegration|Adding an integration to your group will share the cluster across all your projects.') diff --git a/app/presenters/instance_clusterable_presenter.rb b/app/presenters/instance_clusterable_presenter.rb new file mode 100644 index 00000000000..f8bbe5216f1 --- /dev/null +++ b/app/presenters/instance_clusterable_presenter.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +class InstanceClusterablePresenter < ClusterablePresenter + extend ::Gitlab::Utils::Override + include ActionView::Helpers::UrlHelper + + def self.fabricate(clusterable, **attributes) + attributes_with_presenter_class = attributes.merge(presenter_class: InstanceClusterablePresenter) + + Gitlab::View::Presenter::Factory + .new(clusterable, attributes_with_presenter_class) + .fabricate! + end + + override :index_path + def index_path + admin_clusters_path + end + + override :new_path + def new_path + new_admin_cluster_path + end + + override :cluster_status_cluster_path + def cluster_status_cluster_path(cluster, params = {}) + cluster_status_admin_cluster_path(cluster, params) + end + + override :install_applications_cluster_path + def install_applications_cluster_path(cluster, application) + install_applications_admin_cluster_path(cluster, application) + end + + override :update_applications_cluster_path + def update_applications_cluster_path(cluster, application) + update_applications_admin_cluster_path(cluster, application) + end + + override :cluster_path + def cluster_path(cluster, params = {}) + admin_cluster_path(cluster, params) + end + + override :create_user_clusters_path + def create_user_clusters_path + create_user_admin_clusters_path + end + + override :create_gcp_clusters_path + def create_gcp_clusters_path + create_gcp_admin_clusters_path + end + + override :empty_state_help_text + def empty_state_help_text + s_('ClusterIntegration|Adding an integration will share the cluster across all projects.') + end + + override :sidebar_text + def sidebar_text + s_('ClusterIntegration|Adding a Kubernetes cluster will automatically share the cluster across all projects. Use review apps, deploy your applications, and easily run your pipelines for all projects using the same cluster.') + end + + override :learn_more_link + def learn_more_link + link_to(s_('ClusterIntegration|Learn more about instance Kubernetes clusters'), help_page_path('user/instance/clusters/index'), target: '_blank', rel: 'noopener noreferrer') + end +end diff --git a/app/presenters/project_clusterable_presenter.rb b/app/presenters/project_clusterable_presenter.rb index cc0e40e6ab8..8661ee02b68 100644 --- a/app/presenters/project_clusterable_presenter.rb +++ b/app/presenters/project_clusterable_presenter.rb @@ -24,11 +24,6 @@ class ProjectClusterablePresenter < ClusterablePresenter project_cluster_path(clusterable, cluster, params) end - override :clusters_path - def clusters_path(params = {}) - project_clusters_path(clusterable, params) - end - override :sidebar_text def sidebar_text s_('ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way.') diff --git a/app/serializers/test_case_entity.rb b/app/serializers/test_case_entity.rb index ec60055ba5b..5c915c1302c 100644 --- a/app/serializers/test_case_entity.rb +++ b/app/serializers/test_case_entity.rb @@ -3,6 +3,7 @@ class TestCaseEntity < Grape::Entity expose :status expose :name + expose :classname expose :execution_time expose :system_output expose :stack_trace diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 252f5778644..c17712355af 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -104,17 +104,11 @@ module Ci end def schedule_head_pipeline_update - related_merge_requests.each do |merge_request| + pipeline.all_merge_requests.opened.each do |merge_request| UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id) end end - # rubocop: disable CodeReuse/ActiveRecord - def related_merge_requests - pipeline.project.source_of_merge_requests.opened.where(source_branch: pipeline.ref) - end - # rubocop: enable CodeReuse/ActiveRecord - def extra_options(options = {}) # In Ruby 2.4, even when options is empty, f(**options) doesn't work when f # doesn't have any parameters. We reproduce the Ruby 2.5 behavior by diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb index 6707a1363d0..baa3f898b2d 100644 --- a/app/services/ci/register_job_service.rb +++ b/app/services/ci/register_job_service.rb @@ -36,6 +36,11 @@ module Ci builds = builds.with_any_tags end + # pick builds that older than specified age + if params.key?(:job_age) + builds = builds.queued_before(params[:job_age].seconds.ago) + end + builds.each do |build| next unless runner.can_pick?(build) diff --git a/app/services/clusters/build_service.rb b/app/services/clusters/build_service.rb index 8de73831164..b1ac5549e30 100644 --- a/app/services/clusters/build_service.rb +++ b/app/services/clusters/build_service.rb @@ -12,6 +12,8 @@ module Clusters cluster.cluster_type = :project_type when ::Group cluster.cluster_type = :group_type + when Instance + cluster.cluster_type = :instance_type else raise NotImplementedError end diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb index 5a9da053780..886e484caaf 100644 --- a/app/services/clusters/create_service.rb +++ b/app/services/clusters/create_service.rb @@ -38,6 +38,8 @@ module Clusters { cluster_type: :project_type, projects: [clusterable] } when ::Group { cluster_type: :group_type, groups: [clusterable] } + when Instance + { cluster_type: :instance_type } else raise NotImplementedError end diff --git a/app/services/clusters/refresh_service.rb b/app/services/clusters/refresh_service.rb index b02bb9c0247..3752a306793 100644 --- a/app/services/clusters/refresh_service.rb +++ b/app/services/clusters/refresh_service.rb @@ -21,11 +21,7 @@ module Clusters private_class_method :projects_with_missing_kubernetes_namespaces_for_cluster def self.clusters_with_missing_kubernetes_namespaces_for_project(project) - if Feature.enabled?(:ci_preparing_state, default_enabled: true) - project.clusters.managed.missing_kubernetes_namespace(project.kubernetes_namespaces) - else - project.all_clusters.managed.missing_kubernetes_namespace(project.kubernetes_namespaces) - end + project.clusters.managed.missing_kubernetes_namespace(project.kubernetes_namespaces) end private_class_method :clusters_with_missing_kubernetes_namespaces_for_project diff --git a/app/services/concerns/users/participable_service.rb b/app/services/concerns/users/participable_service.rb index a3cc6014fd3..1c828234f1b 100644 --- a/app/services/concerns/users/participable_service.rb +++ b/app/services/concerns/users/participable_service.rb @@ -29,7 +29,7 @@ module Users def groups group_counts = GroupMember - .in_groups(current_user.authorized_groups) + .of_groups(current_user.authorized_groups) .non_request .count_users_by_group_id diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb index 9d371e234ee..d30df34e54b 100644 --- a/app/services/git/base_hooks_service.rb +++ b/app/services/git/base_hooks_service.rb @@ -17,6 +17,8 @@ module Git # Not a hook, but it needs access to the list of changed commits enqueue_invalidate_cache + update_remote_mirrors + push_data end @@ -92,5 +94,12 @@ module Git def pipeline_options {} end + + def update_remote_mirrors + return unless project.has_remote_mirror? + + project.mark_stuck_remote_mirrors_as_failed! + project.update_remote_mirrors + end end end diff --git a/app/services/git/branch_push_service.rb b/app/services/git/branch_push_service.rb index da053ce80c7..c4910180787 100644 --- a/app/services/git/branch_push_service.rb +++ b/app/services/git/branch_push_service.rb @@ -27,7 +27,6 @@ module Git execute_related_hooks perform_housekeeping - update_remote_mirrors stop_environments true @@ -48,7 +47,7 @@ module Git def enqueue_detect_repository_languages return unless default_branch? - DetectRepositoryLanguagesWorker.perform_async(project.id, current_user.id) + DetectRepositoryLanguagesWorker.perform_async(project.id) end # Only stop environments if the ref is a branch that is being deleted diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb index e5cc12e6082..2a19e57a94f 100644 --- a/app/services/issues/close_service.rb +++ b/app/services/issues/close_service.rb @@ -7,7 +7,7 @@ module Issues return issue unless can?(current_user, :update_issue, issue) close_issue(issue, - commit: commit, + closed_via: commit, notifications: notifications, system_note: system_note) end @@ -17,9 +17,9 @@ module Issues # # The code calling this method is responsible for ensuring that a user is # allowed to close the given issue. - def close_issue(issue, commit: nil, notifications: true, system_note: true) + def close_issue(issue, closed_via: nil, notifications: true, system_note: true) if project.jira_tracker? && project.jira_service.active && issue.is_a?(ExternalIssue) - project.jira_service.close_issue(commit, issue) + project.jira_service.close_issue(closed_via, issue) todo_service.close_issue(issue, current_user) return issue end @@ -27,8 +27,11 @@ module Issues if project.issues_enabled? && issue.close issue.update(closed_by: current_user) event_service.close_issue(issue, current_user) - create_note(issue, commit) if system_note - notification_service.async.close_issue(issue, current_user) if notifications + create_note(issue, closed_via) if system_note + + closed_via = "commit #{closed_via.id}" if closed_via.is_a?(Commit) + + notification_service.async.close_issue(issue, current_user, closed_via: closed_via) if notifications todo_service.close_issue(issue, current_user) execute_hooks(issue, 'close') invalidate_cache_counts(issue, users: issue.assignees) diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb index f6bad74736c..d6b17ec10be 100644 --- a/app/services/members/create_service.rb +++ b/app/services/members/create_service.rb @@ -23,7 +23,16 @@ module Members members.each do |member| if member.errors.any? - errors << "#{member.user.username}: #{member.errors.full_messages.to_sentence}" + current_error = + # Invited users may not have an associated user + if member.user.present? + "#{member.user.username}: " + else + "" + end + + current_error += member.errors.full_messages.to_sentence + errors << current_error else after_execute(member: member) end diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb index f9717a9426b..c8d5e563cd8 100644 --- a/app/services/members/destroy_service.rb +++ b/app/services/members/destroy_service.rb @@ -45,7 +45,7 @@ module Members def delete_subgroup_members(member) groups = member.group.descendants - GroupMember.in_groups(groups).with_user(member.user).each do |group_member| + GroupMember.of_groups(groups).with_user(member.user).each do |group_member| self.class.new(current_user).execute(group_member, skip_authorization: @skip_auth, skip_subresources: true) end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 8d3b569498f..f797c0f11c6 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -89,8 +89,8 @@ class NotificationService # * project team members with notification level higher then Participating # * users with custom level checked with "close issue" # - def close_issue(issue, current_user) - close_resource_email(issue, current_user, :closed_issue_email) + def close_issue(issue, current_user, closed_via: nil) + close_resource_email(issue, current_user, :closed_issue_email, closed_via: closed_via) end # When we reassign an issue we should send an email to: @@ -504,7 +504,7 @@ class NotificationService end end - def close_resource_email(target, current_user, method, skip_current_user: true) + def close_resource_email(target, current_user, method, skip_current_user: true, closed_via: nil) action = method == :merged_merge_request_email ? "merge" : "close" recipients = NotificationRecipientService.build_recipients( @@ -515,7 +515,7 @@ class NotificationService ) recipients.each do |recipient| - mailer.send(method, recipient.user.id, target.id, current_user.id, recipient.reason).deliver_later + mailer.send(method, recipient.user.id, target.id, current_user.id, reason: recipient.reason, closed_via: closed_via).deliver_later end end diff --git a/app/services/projects/after_import_service.rb b/app/services/projects/after_import_service.rb index afb9048e87b..bbdde4408d2 100644 --- a/app/services/projects/after_import_service.rb +++ b/app/services/projects/after_import_service.rb @@ -9,7 +9,7 @@ module Projects end def execute - Projects::HousekeepingService.new(@project, :gc).execute do + Projects::HousekeepingService.new(@project).execute do repository.delete_all_refs_except(RESERVED_REF_PREFIXES) end rescue Projects::HousekeepingService::LeaseTaken => e diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 4ea40e3c8ce..9f335cceb67 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -100,8 +100,6 @@ module Projects current_user.invalidate_personal_projects_count create_readme if @initialize_with_readme - - configure_group_clusters_for_project end # Refresh the current user's authorizations inline (so they can access the @@ -127,10 +125,6 @@ module Projects Files::CreateService.new(@project, current_user, commit_attrs).execute end - def configure_group_clusters_for_project - ClusterProjectConfigureWorker.perform_async(@project.id) - end - def skip_wiki? !@project.feature_available?(:wiki, current_user) || @skip_wiki end diff --git a/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb index 05974948505..9b72480d18b 100644 --- a/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb +++ b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb @@ -37,7 +37,17 @@ module Projects raise DownloadLinksError, response.message unless response.success? - parse_response_links(response['objects']) + # Since the LFS Batch API may return a Content-Ttpe of + # application/vnd.git-lfs+json + # (https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md#requests), + # HTTParty does not know this is actually JSON. + data = JSON.parse(response.body) + + raise DownloadLinksError, "LFS Batch API did return any objects" unless data.is_a?(Hash) && data.key?('objects') + + parse_response_links(data['objects']) + rescue JSON::ParserError + raise DownloadLinksError, "LFS Batch API response is not JSON" end def parse_response_links(objects_response) diff --git a/app/services/projects/repository_languages_service.rb b/app/services/projects/repository_languages_service.rb index e75851c7da4..05f43c2264b 100644 --- a/app/services/projects/repository_languages_service.rb +++ b/app/services/projects/repository_languages_service.rb @@ -11,7 +11,7 @@ module Projects def perform_language_detection if persisted_repository_languages.blank? - ::DetectRepositoryLanguagesWorker.perform_async(project.id, current_user.id) + ::DetectRepositoryLanguagesWorker.perform_async(project.id) else project.update_column(:detected_repository_languages, true) end diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 91c01eca75c..233dcf37e35 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -54,7 +54,6 @@ module Projects end attempt_transfer_transaction - configure_group_clusters_for_project end # rubocop: enable CodeReuse/ActiveRecord @@ -164,9 +163,5 @@ module Projects @new_namespace.full_path ) end - - def configure_group_clusters_for_project - ClusterProjectConfigureWorker.perform_async(project.id) - end end end diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb index 0a166335b4e..b488bba00e9 100644 --- a/app/uploaders/attachment_uploader.rb +++ b/app/uploaders/attachment_uploader.rb @@ -9,6 +9,6 @@ class AttachmentUploader < GitlabUploader private def dynamic_segment - File.join(model.class.to_s.underscore, mounted_as.to_s, model.id.to_s) + File.join(model.class.underscore, mounted_as.to_s, model.id.to_s) end end diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb index c0165759203..9af59b0aceb 100644 --- a/app/uploaders/avatar_uploader.rb +++ b/app/uploaders/avatar_uploader.rb @@ -25,6 +25,6 @@ class AvatarUploader < GitlabUploader private def dynamic_segment - File.join(model.class.to_s.underscore, mounted_as.to_s, model.id.to_s) + File.join(model.class.underscore, mounted_as.to_s, model.id.to_s) end end diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 6dfe2bed0ba..1c7582533ad 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -109,12 +109,20 @@ class FileUploader < GitlabUploader def upload_path if file_storage? # Legacy path relative to project.full_path - File.join(dynamic_segment, identifier) + local_storage_path(identifier) else - File.join(store_dir, identifier) + remote_storage_path(identifier) end end + def local_storage_path(file_identifier) + File.join(dynamic_segment, file_identifier) + end + + def remote_storage_path(file_identifier) + File.join(store_dir, file_identifier) + end + def store_dirs { Store::LOCAL => File.join(base_dir, dynamic_segment), diff --git a/app/uploaders/import_export_uploader.rb b/app/uploaders/import_export_uploader.rb index 716922bc017..104d5d3b3dd 100644 --- a/app/uploaders/import_export_uploader.rb +++ b/app/uploaders/import_export_uploader.rb @@ -7,10 +7,6 @@ class ImportExportUploader < AttachmentUploader EXTENSION_WHITELIST end - def move_to_store - true - end - def move_to_cache false end diff --git a/app/uploaders/personal_file_uploader.rb b/app/uploaders/personal_file_uploader.rb index 272837aa6ce..b43162f0935 100644 --- a/app/uploaders/personal_file_uploader.rb +++ b/app/uploaders/personal_file_uploader.rb @@ -6,21 +6,18 @@ class PersonalFileUploader < FileUploader options.storage_path end - def self.base_dir(model, store = nil) - base_dirs(model)[store || Store::LOCAL] - end - - def self.base_dirs(model) - { - Store::LOCAL => File.join(options.base_dir, model_path_segment(model)), - Store::REMOTE => model_path_segment(model) - } + def self.base_dir(model, _store = nil) + # base_dir is the path seen by the user when rendering Markdown, so + # it should be the same for both local and object storage. It is + # typically prefaced with uploads/-/system, but that prefix + # is omitted in the path stored on disk. + File.join(options.base_dir, model_path_segment(model)) end def self.model_path_segment(model) return 'temp/' unless model - File.join(model.class.to_s.underscore, model.id.to_s) + File.join(model.class.underscore, model.id.to_s) end def object_store @@ -40,8 +37,61 @@ class PersonalFileUploader < FileUploader store_dirs[object_store] end + # A personal snippet path is stored using FileUploader#upload_path. + # + # The format for the path: + # + # Local storage: :random_hex/:filename. + # Object storage: personal_snippet/:id/:random_hex/:filename. + # + # upload_paths represent the possible paths for a given identifier, + # which will vary depending on whether the file is stored in local or + # object storage. upload_path should match an element in upload_paths. + # + # base_dir represents the path seen by the user in Markdown, and it + # should always be prefixed with uploads/-/system. + # + # store_dirs represent the paths that are actually used on disk. For + # object storage, this should omit the prefix /uploads/-/system. + # + # For example, consider the requested path /uploads/-/system/personal_snippet/172/ff4ad5c2e40b39ae57cda51577317d20/file.png. + # + # For local storage: + # + # File on disk: /opt/gitlab/embedded/service/gitlab-rails/public/uploads/-/system/personal_snippet/172/ff4ad5c2e40b39ae57cda51577317d20/file.png. + # + # base_dir: uploads/-/system/personal_snippet/172 + # upload_path: ff4ad5c2e40b39ae57cda51577317d20/file.png + # upload_paths: ["ff4ad5c2e40b39ae57cda51577317d20/file.png", "personal_snippet/172/ff4ad5c2e40b39ae57cda51577317d20/file.png"]. + # store_dirs: + # => {1=>"uploads/-/system/personal_snippet/172/ff4ad5c2e40b39ae57cda51577317d20", 2=>"personal_snippet/172/ff4ad5c2e40b39ae57cda51577317d20"} + # + # For object storage: + # + # upload_path: personal_snippet/172/ff4ad5c2e40b39ae57cda51577317d20/file.png + def upload_paths(identifier) + [ + local_storage_path(identifier), + File.join(remote_storage_base_path, identifier) + ] + end + + def store_dirs + { + Store::LOCAL => File.join(base_dir, dynamic_segment), + Store::REMOTE => remote_storage_base_path + } + end + private + # To avoid prefacing the remote storage path with `/uploads/-/system`, + # we just drop that part so that the destination path will be + # personal_snippet/:id/:random_hex/:filename. + def remote_storage_base_path + File.join(self.class.model_path_segment(model), dynamic_segment) + end + def secure_url File.join('/', base_dir, secret, file.filename) end diff --git a/app/views/admin/application_settings/_pages.html.haml b/app/views/admin/application_settings/_pages.html.haml index 64e01fa2d00..77795dbf913 100644 --- a/app/views/admin/application_settings/_pages.html.haml +++ b/app/views/admin/application_settings/_pages.html.haml @@ -30,8 +30,7 @@ .form-check = f.check_box :lets_encrypt_terms_of_service_accepted, class: 'form-check-input' = f.label :lets_encrypt_terms_of_service_accepted, class: 'form-check-label' do - // Terms of Service should actually be a link, but the best way to get the url is using API - // So it will be done in later MR - = _("I have read and agree to the Let's Encrypt Terms of Service") + - terms_of_service_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: lets_encrypt_terms_of_service_admin_application_settings_path } + = _("I have read and agree to the Let's Encrypt %{link_start}Terms of Service%{link_end}").html_safe % { link_start: terms_of_service_link_start, link_end: '</a>'.html_safe } = f.submit _('Save changes'), class: "btn btn-success" diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index 8fb38f6a690..62d1d01cc83 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -2,7 +2,7 @@ = form_errors(@group) = render 'shared/group_form', f: f - = render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group + = render_if_exists 'shared/old_repository_size_limit_setting', form: f, type: :group = render_if_exists 'admin/namespace_plan', f: f .form-group.row.group-description-holder diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml index 0f5e97e288a..ac56e354a4d 100644 --- a/app/views/admin/health_check/show.html.haml +++ b/app/views/admin/health_check/show.html.haml @@ -23,7 +23,7 @@ %code= liveness_url(token: Gitlab::CurrentSettings.health_check_access_token) %li %code= metrics_url(token: Gitlab::CurrentSettings.health_check_access_token) - + = render_if_exists 'admin/health_check/health_check_url' %hr .card .card-header diff --git a/app/views/admin/projects/_projects.html.haml b/app/views/admin/projects/_projects.html.haml index 5bc695aa7b5..9117f63f939 100644 --- a/app/views/admin/projects/_projects.html.haml +++ b/app/views/admin/projects/_projects.html.haml @@ -13,7 +13,7 @@ .stats %span.badge.badge-pill - = storage_counter(project.statistics.storage_size) + = storage_counter(project.statistics&.storage_size) - if project.archived %span.badge.badge-warning archived .title diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 46bb57c78a8..b88b760536d 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -7,7 +7,7 @@ .top-area.scrolling-tabs-container.inner-page-scroll-tabs .prepend-top-default .search-holder - = render 'shared/projects/search_form', autofocus: true, icon: true + = render 'shared/projects/search_form', autofocus: true, icon: true, admin_view: true .dropdown - toggle_text = 'Namespace' - if params[:namespace_id].present? diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index bc34af88928..1e1ad9d5e19 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -74,10 +74,10 @@ %li %span.light= _('Storage:') - %strong= storage_counter(@project.statistics.storage_size) - ( - = storage_counters_details(@project.statistics) - ) + %strong= storage_counter(@project.statistics&.storage_size) + - if @project.statistics + = surround '(', ')' do + = storage_counters_details(@project.statistics) %li %span.light last commit: @@ -100,6 +100,8 @@ %span.light archived: %strong project is read-only + = render_if_exists "shared_runner_status", project: @project + %li %span.light access: %strong diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml index 12e24ddef02..98b6bc7bc46 100644 --- a/app/views/admin/users/_access_levels.html.haml +++ b/app/views/admin/users/_access_levels.html.haml @@ -22,6 +22,8 @@ %p.light Regular users have access to their groups and projects + = render_if_exists 'admin/users/auditor_access_level_radio', f: f, disabled: editing_current_user + = f.radio_button :access_level, :admin, disabled: editing_current_user = label_tag :admin, class: 'font-weight-bold' do Admin diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml index a733f420d11..e7dde7985fd 100644 --- a/app/views/admin/users/_head.html.haml +++ b/app/views/admin/users/_head.html.haml @@ -6,6 +6,7 @@ %span.cred (Internal) - if @user.admin %span.cred (Admin) + = render_if_exists 'admin/users/audtior_user_badge' .float-right - if impersonation_enabled? && @user != current_user && @user.can?(:log_in) diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml index dc9ccb6cc39..464b9faf282 100644 --- a/app/views/ci/variables/_index.html.haml +++ b/app/views/ci/variables/_index.html.haml @@ -10,6 +10,7 @@ .hide.alert.alert-danger.js-ci-variable-error-box %ul.ci-variable-list + = render 'ci/variables/variable_header' - @variables.each.each do |variable| = render 'ci/variables/variable_row', form_field: 'variables', variable: variable = render 'ci/variables/variable_row', form_field: 'variables' diff --git a/app/views/ci/variables/_variable_header.html.haml b/app/views/ci/variables/_variable_header.html.haml new file mode 100644 index 00000000000..d3b7a5ae883 --- /dev/null +++ b/app/views/ci/variables/_variable_header.html.haml @@ -0,0 +1,16 @@ +- only_key_value = local_assigns.fetch(:only_key_value, false) + +%li.ci-variable-row.m-0.d-none.d-sm-block + .d-flex.w-100.align-items-center.pb-2 + .bold.table-section.section-15.append-right-10 + = s_('CiVariables|Type') + .bold.table-section.section-15.append-right-10 + = s_('CiVariables|Key') + .bold.table-section.section-15.append-right-10 + = s_('CiVariables|Value') + - unless only_key_value + .bold.table-section.section-20 + = s_('CiVariables|State') + .bold.table-section.section-20 + = s_('CiVariables|Masked') + = render_if_exists 'ci/variables/environment_scope_header' diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml index 37257b3aa1c..b4930b41c09 100644 --- a/app/views/ci/variables/_variable_row.html.haml +++ b/app/views/ci/variables/_variable_row.html.haml @@ -20,18 +20,18 @@ - masked_input_name = "#{form_field}[variables_attributes][][masked]" %li.js-row.ci-variable-row{ data: { is_persisted: "#{!id.nil?}" } } - .ci-variable-row-body + .ci-variable-row-body.border-bottom %input.js-ci-variable-input-id{ type: "hidden", name: id_input_name, value: id } %input.js-ci-variable-input-destroy{ type: "hidden", name: destroy_input_name } - %select.js-ci-variable-input-variable-type.ci-variable-body-item.form-control.select-control{ name: variable_type_input_name } + %select.js-ci-variable-input-variable-type.ci-variable-body-item.form-control.select-control.table-section.section-15{ name: variable_type_input_name } = options_for_select(ci_variable_type_options, variable_type) - %input.js-ci-variable-input-key.ci-variable-body-item.qa-ci-variable-input-key.form-control{ type: "text", + %input.js-ci-variable-input-key.ci-variable-body-item.qa-ci-variable-input-key.form-control.table-section.section-15{ type: "text", name: key_input_name, value: key, placeholder: s_('CiVariables|Input variable key') } - .ci-variable-body-item.gl-show-field-errors + .ci-variable-body-item.gl-show-field-errors.table-section.section-15.border-top-0.p-0 .form-control.js-secret-value-placeholder.qa-ci-variable-input-value{ class: ('hide' unless id) } - = '*' * 20 + = '*' * 17 %textarea.js-ci-variable-input-value.js-secret-value.qa-ci-variable-input-value.form-control{ class: ('hide' if id), rows: 1, name: value_input_name, @@ -41,7 +41,7 @@ = s_("CiVariables|Cannot use Masked Variable with current value") = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'masked-variables'), target: '_blank', rel: 'noopener noreferrer' - unless only_key_value - .ci-variable-body-item.ci-variable-protected-item + .ci-variable-body-item.ci-variable-protected-item.table-section.section-20.mr-0.border-top-0 .append-right-default = s_("CiVariable|Protected") %button{ type: 'button', @@ -55,7 +55,7 @@ %span.toggle-icon = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') - .ci-variable-body-item.ci-variable-masked-item + .ci-variable-body-item.ci-variable-masked-item.table-section.section-20.mr-0.border-top-0 .append-right-default = s_("CiVariable|Masked") %button{ type: 'button', @@ -70,5 +70,5 @@ = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') = render_if_exists 'ci/variables/environment_scope', form_field: form_field, variable: variable - %button.js-row-remove-button.ci-variable-row-remove-button{ type: 'button', 'aria-label': s_('CiVariables|Remove variable row') } - = icon('minus-circle') + %button.js-row-remove-button.ci-variable-row-remove-button.table-section.section-5.border-top-0{ type: 'button', 'aria-label': s_('CiVariables|Remove variable row') } + = icon('minus-circle') diff --git a/app/views/clusters/platforms/kubernetes/_form.html.haml b/app/views/clusters/platforms/kubernetes/_form.html.haml index 8caa25a7b5e..c1727cf9079 100644 --- a/app/views/clusters/platforms/kubernetes/_form.html.haml +++ b/app/views/clusters/platforms/kubernetes/_form.html.haml @@ -1,7 +1,7 @@ = bootstrap_form_for cluster, url: update_cluster_url_path, html: { class: 'gl-show-field-errors' }, as: :cluster do |field| - copy_name_btn = clipboard_button(text: cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), - class: 'input-group-text btn-default') unless !cluster.read_only_kubernetes_platform_fields? + class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields? = field.text_field :name, class: 'js-select-on-focus cluster-name', required: true, title: s_('ClusterIntegration|Cluster name is required.'), readonly: cluster.read_only_kubernetes_platform_fields?, @@ -10,7 +10,7 @@ = field.fields_for :platform_kubernetes, platform do |platform_field| - copy_api_url = clipboard_button(text: platform.api_url, title: s_('ClusterIntegration|Copy API URL'), - class: 'input-group-text btn-default') unless !cluster.read_only_kubernetes_platform_fields? + class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields? = platform_field.text_field :api_url, class: 'js-select-on-focus', required: true, title: s_('ClusterIntegration|API URL should be a valid http/https url.'), readonly: cluster.read_only_kubernetes_platform_fields?, @@ -18,7 +18,7 @@ input_group_class: 'gl-field-error-anchor', append: copy_api_url - copy_ca_cert_btn = clipboard_button(text: platform.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'), - class: 'input-group-text btn-default') unless !cluster.read_only_kubernetes_platform_fields? + class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields? = platform_field.text_area :ca_cert, class: 'js-select-on-focus', rows: '5', readonly: cluster.read_only_kubernetes_platform_fields?, placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'), @@ -28,7 +28,7 @@ - show_token_btn = (platform_field.button s_('ClusterIntegration|Show'), type: 'button', class: 'js-show-cluster-token btn btn-default') - copy_token_btn = clipboard_button(text: platform.token, title: s_('ClusterIntegration|Copy Service Token'), - class: 'input-group-text btn-default') unless !cluster.read_only_kubernetes_platform_fields? + class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields? = platform_field.text_field :token, type: 'password', class: 'js-select-on-focus js-cluster-token', required: true, title: s_('ClusterIntegration|Service token is required.'), diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index ca2822e2b29..97a446dbeec 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -1,3 +1,6 @@ +- project_tab_filter = local_assigns.fetch(:project_tab_filter, "") +- feature_project_list_filter_bar = Feature.enabled?(:project_list_filter_bar) + = content_for :flash_message do = render 'shared/project_limit' @@ -6,24 +9,27 @@ - if current_user.can_create_project? .page-title-controls - = link_to "New project", new_project_path, class: "btn btn-success" + = link_to _("New project"), new_project_path, class: "btn btn-success" .top-area.scrolling-tabs-container.inner-page-scroll-tabs .fade-left= icon('angle-left') .fade-right= icon('angle-right') - %ul.nav-links.scrolling-tabs.mobile-separator.nav.nav-tabs + %ul.nav-links.scrolling-tabs.mobile-separator.nav.nav-tabs{ class: ('border-0' if feature_project_list_filter_bar) } = nav_link(page: [dashboard_projects_path, root_path]) do = link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do - Your projects + = _("Your projects") %span.badge.badge-pill= limited_counter_with_delimiter(@total_user_projects_count) = nav_link(page: starred_dashboard_projects_path) do = link_to starred_dashboard_projects_path, data: {placement: 'right'} do - Starred projects + = _("Starred projects") %span.badge.badge-pill= limited_counter_with_delimiter(@total_starred_projects_count) = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do = link_to explore_root_path, data: {placement: 'right'} do - Explore projects - - .nav-controls - = render 'shared/projects/search_form' - = render 'shared/projects/dropdown' + = _("Explore projects") + - unless feature_project_list_filter_bar + .nav-controls + = render 'shared/projects/search_form' + = render 'shared/projects/dropdown' +- if feature_project_list_filter_bar + .project-filters + = render 'shared/projects/search_bar', project_tab_filter: project_tab_filter diff --git a/app/views/dashboard/projects/_nav.html.haml b/app/views/dashboard/projects/_nav.html.haml index da3cf5807b0..f9b61bf1f3e 100644 --- a/app/views/dashboard/projects/_nav.html.haml +++ b/app/views/dashboard/projects/_nav.html.haml @@ -1,6 +1,21 @@ -.nav-block - %ul.nav-links.mobile-separator.nav.nav-tabs - = nav_link(html_options: { class: ("active" unless params[:personal].present?) }) do - = link_to s_('DashboardProjects|All'), dashboard_projects_path - = nav_link(html_options: { class: ("active" if params[:personal].present?) }) do - = link_to s_('DashboardProjects|Personal'), filter_projects_path(personal: true) +- inactive_class = 'btn p-2' +- active_class = 'btn p-2 active' +- project_tab_filter = local_assigns.fetch(:project_tab_filter, "") +- is_explore_trending = project_tab_filter == :explore_trending +- feature_project_list_filter_bar = Feature.enabled?(:project_list_filter_bar) + +.nav-block{ class: ("w-100" if feature_project_list_filter_bar) } + - if feature_project_list_filter_bar + .btn-group.button-filter-group.d-flex.m-0.p-0 + - if project_tab_filter == :explore || is_explore_trending + = link_to s_('DashboardProjects|Trending'), trending_explore_projects_path, class: is_explore_trending ? active_class : inactive_class + = link_to s_('DashboardProjects|All'), explore_projects_path, class: is_explore_trending ? inactive_class : active_class + - else + = link_to s_('DashboardProjects|All'), dashboard_projects_path, class: params[:personal].present? ? inactive_class : active_class + = link_to s_('DashboardProjects|Personal'), filter_projects_path(personal: true), class: params[:personal].present? ? active_class : inactive_class + - else + %ul.nav-links.mobile-separator.nav.nav-tabs + = nav_link(html_options: { class: ("active" unless params[:personal].present?) }) do + = link_to s_('DashboardProjects|All'), dashboard_projects_path + = nav_link(html_options: { class: ("active" if params[:personal].present?) }) do + = link_to s_('DashboardProjects|Personal'), filter_projects_path(personal: true) diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml index 18a82feb189..8933c5d7227 100644 --- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml +++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml @@ -1,4 +1,4 @@ -.blank-state-parent-container +.blank-state-parent-container{ class: ('has-start-trial-container' if has_start_trial?) } .section-container.section-welcome{ class: "#{ 'section-admin-welcome' if current_user.admin? }" } .container.section-body .row @@ -7,7 +7,12 @@ Welcome to GitLab %p.blank-state-text Code, test, and deploy together - - if current_user.admin? - = render "blank_state_admin_welcome" - - else - = render "blank_state_welcome" + .blank-state-row + %div{ class: ('column-large' if has_start_trial?) } + - if current_user.admin? + = render "blank_state_admin_welcome" + - else + = render "blank_state_welcome" + - if has_start_trial? + .column-small + = render_if_exists "blank_state_ee_trial" diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml index dc9468b3368..0298f539b4b 100644 --- a/app/views/dashboard/projects/index.html.haml +++ b/app/views/dashboard/projects/index.html.haml @@ -13,7 +13,7 @@ = render "projects/last_push" - if show_projects?(@projects, params) = render 'dashboard/projects_head' - = render 'nav' + = render 'nav' unless Feature.enabled?(:project_list_filter_bar) = render 'projects' - else = render "zero_authorized_projects" diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml index a0d85446e5f..0fcc6894b68 100644 --- a/app/views/dashboard/projects/starred.html.haml +++ b/app/views/dashboard/projects/starred.html.haml @@ -8,7 +8,7 @@ %div{ class: container_class } = render "projects/last_push" - = render 'dashboard/projects_head' + = render 'dashboard/projects_head', project_tab_filter: :starred - if params[:filter_projects] || any_projects?(@projects) = render 'projects' diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index efe1fb99efc..db6e40a6fd0 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -34,7 +34,7 @@ = todo_due_date(todo) .todo-body - .todo-note + .todo-note.break-word .md = first_line_in_markdown(todo, :body, 150, project: todo.project) diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 9c7ca6ebbd4..427db070253 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -29,6 +29,7 @@ - terms_link = link_to s_("I accept the|Terms of Service and Privacy Policy"), terms_path, target: "_blank" - accept_terms_label = _("I accept the %{terms_link}") % { terms_link: terms_link } = accept_terms_label.html_safe + = render_if_exists 'devise/shared/email_opted_in', f: f %div - if Gitlab::Recaptcha.enabled? = recaptcha_tags diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml index aee05b6c81c..b1a9470cf1c 100644 --- a/app/views/devise/shared/_tabs_ldap.html.haml +++ b/app/views/devise/shared/_tabs_ldap.html.haml @@ -2,6 +2,7 @@ - if crowd_enabled? %li.nav-item = link_to "Crowd", "#crowd", class: "nav-link #{active_when(form_based_auth_provider_has_active_class?(:crowd))}", 'data-toggle' => 'tab' + = render_if_exists "devise/shared/kerberos_tab" - @ldap_servers.each_with_index do |server, i| %li.nav-item = link_to server['label'], "##{server['provider_name']}", class: "nav-link #{active_when(i.zero? && form_based_auth_provider_has_active_class?(:ldapmain))} qa-ldap-tab", 'data-toggle' => 'tab' diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index 2fcb1d1fd2b..222175c818a 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -3,11 +3,11 @@ .event-item-timestamp #{time_ago_with_tooltip(event.created_at)} - - if event.created_project? + - if event.created_project_action? = render "events/event/created_project", event: event - - elsif event.push? + - elsif event.push_action? = render "events/event/push", event: event - - elsif event.commented? + - elsif event.commented_action? = render "events/event/note", event: event - else = render "events/event/common", event: event diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml index f518205f14c..d00a3d266d8 100644 --- a/app/views/explore/projects/_filter.html.haml +++ b/app/views/explore/projects/_filter.html.haml @@ -1,8 +1,12 @@ +- has_label = local_assigns.fetch(:has_label, false) +- feature_project_list_filter_bar = Feature.enabled?(:project_list_filter_bar) + - if current_user - .dropdown + .dropdown.js-project-filter-dropdown-wrap{ class: ('d-flex flex-grow-1 flex-shrink-1' if feature_project_list_filter_bar) } %button.dropdown-menu-toggle{ href: '#', "data-toggle" => "dropdown", 'data-display' => 'static' } - = icon('globe', class: 'mt-1') - %span.light.ml-3= _("Visibility:") + - unless has_label + = icon('globe', class: 'mt-1') + %span.light.ml-3= _("Visibility:") - if params[:visibility_level].present? = visibility_level_label(params[:visibility_level].to_i) - else diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml index dd2bf6a5ef8..341ad681c7c 100644 --- a/app/views/explore/projects/index.html.haml +++ b/app/views/explore/projects/index.html.haml @@ -5,9 +5,9 @@ = render_dashboard_gold_trial(current_user) - if current_user - = render 'dashboard/projects_head' + = render 'dashboard/projects_head', project_tab_filter: :explore - else = render 'explore/head' -= render 'explore/projects/nav' += render 'explore/projects/nav' unless Feature.enabled?(:project_list_filter_bar) && current_user = render 'projects', projects: @projects diff --git a/app/views/explore/projects/starred.html.haml b/app/views/explore/projects/starred.html.haml index dd2bf6a5ef8..ec92852ddde 100644 --- a/app/views/explore/projects/starred.html.haml +++ b/app/views/explore/projects/starred.html.haml @@ -5,9 +5,9 @@ = render_dashboard_gold_trial(current_user) - if current_user - = render 'dashboard/projects_head' + = render 'dashboard/projects_head', project_tab_filter: :starred - else = render 'explore/head' -= render 'explore/projects/nav' += render 'explore/projects/nav' unless Feature.enabled?(:project_list_filter_bar) && current_user = render 'projects', projects: @projects diff --git a/app/views/explore/projects/trending.html.haml b/app/views/explore/projects/trending.html.haml index dd2bf6a5ef8..ed508fa2506 100644 --- a/app/views/explore/projects/trending.html.haml +++ b/app/views/explore/projects/trending.html.haml @@ -5,9 +5,9 @@ = render_dashboard_gold_trial(current_user) - if current_user - = render 'dashboard/projects_head' + = render 'dashboard/projects_head', project_tab_filter: :explore_trending - else = render 'explore/head' -= render 'explore/projects/nav' += render 'explore/projects/nav' unless Feature.enabled?(:project_list_filter_bar) && current_user = render 'projects', projects: @projects diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index 09cc713e3af..021c0b6c429 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -14,6 +14,8 @@ = render 'shared/members/requests', membership_source: @group, requesters: @requesters + = render_if_exists 'groups/group_members/ldap_sync' + .clearfix %h5.member.existing-title Existing members diff --git a/app/views/groups/settings/_general.html.haml b/app/views/groups/settings/_general.html.haml index c382a1ed168..e12748666c8 100644 --- a/app/views/groups/settings/_general.html.haml +++ b/app/views/groups/settings/_general.html.haml @@ -17,17 +17,17 @@ = f.label :description, _('Group description (optional)'), class: 'label-bold' = f.text_area :description, class: 'form-control', rows: 3, maxlength: 250 - = render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group + = render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group - .form-group.prepend-top-default.append-bottom-20 - .avatar-container.rect-avatar.s90 - = group_icon(@group, alt: '', class: 'avatar group-avatar s90') - = f.label :avatar, _('Group avatar'), class: 'label-bold d-block' - = render 'shared/choose_avatar_button', f: f - - if @group.avatar? - %hr - = link_to _('Remove avatar'), group_avatar_path(@group.to_param), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'btn btn-link' + .form-group.prepend-top-default.append-bottom-20 + .avatar-container.rect-avatar.s90 + = group_icon(@group, alt: '', class: 'avatar group-avatar s90') + = f.label :avatar, _('Group avatar'), class: 'label-bold d-block' + = render 'shared/choose_avatar_button', f: f + - if @group.avatar? + %hr + = link_to _('Remove avatar'), group_avatar_path(@group.to_param), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'btn btn-link' - = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group + = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group = f.submit _('Save changes'), class: 'btn btn-success mt-4 js-dirty-submit' diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index 75e4dc46c9b..50933c7d434 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -5,8 +5,7 @@ %hr %h1 - GitLab - Community Edition + = default_brand_title - if user_signed_in? %span= link_to_version = version_status_badge diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index 969df69aafb..cdc894ee5a0 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -70,7 +70,7 @@ .cover-title John Smith - .cover-desc + .cover-desc.cgray = lorem .cover-controls diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 11e83ddfe64..c357207054b 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -77,3 +77,4 @@ = render 'layouts/google_analytics' if extra_config.has_key?('google_analytics_id') = render 'layouts/piwik' if extra_config.has_key?('piwik_url') && extra_config.has_key?('piwik_site_id') + = render_if_exists 'layouts/snowplow' diff --git a/app/views/layouts/_mailer.html.haml b/app/views/layouts/_mailer.html.haml index e13490ed410..6e8294d6adc 100644 --- a/app/views/layouts/_mailer.html.haml +++ b/app/views/layouts/_mailer.html.haml @@ -64,6 +64,8 @@ %tbody = yield + = render_if_exists 'layouts/mailer/additional_text' + %tr.footer %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } %img{ alt: "GitLab", height: "33", src: image_url('mailers/gitlab_footer_logo.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/ diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 26a1f1e119c..006334ade07 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -5,6 +5,7 @@ = render 'shared/outdated_browser' .mobile-overlay .alert-wrapper + = render_if_exists "layouts/header/ee_license_banner" = render "layouts/broadcast" = render "layouts/header/read_only_banner" = render "layouts/nav/classification_level_banner" diff --git a/app/views/layouts/_piwik.html.haml b/app/views/layouts/_piwik.html.haml index a888e8ae187..473b14ce626 100644 --- a/app/views/layouts/_piwik.html.haml +++ b/app/views/layouts/_piwik.html.haml @@ -7,7 +7,7 @@ (function() { var u="//#{extra_config.piwik_url}/"; _paq.push(['setTrackerUrl', u+'piwik.php']); - _paq.push(['setSiteId', #{extra_config.piwik_site_id}]); + _paq.push(['setSiteId', "#{extra_config.piwik_site_id}"]); var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s); })(); diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml index 2f3c13aaf6e..f7a561afbb3 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise.html.haml @@ -26,6 +26,9 @@ - if Gitlab::CurrentSettings.sign_in_text.present? = markdown_field(Gitlab::CurrentSettings.current_application_settings, :sign_in_text) + + = render_if_exists 'layouts/devise_help_text' + .col-sm-5.new-session-forms-container = yield diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 319d0307f78..f8b7d0c530a 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -17,8 +17,10 @@ - if logo_text.present? %span.logo-text.d-none.d-lg-block.prepend-left-8 = logo_text - %span.js-canary-badge.badge.badge-pill.green-badge.align-self-center - = _('Next') + - if Gitlab.com? + = link_to 'https://next.gitlab.com', class: 'label-link js-canary-badge canary-badge bg-transparent hidden', target: :_blank do + %span.color-label.has-tooltip.badge.badge-pill.green-badge + = _('Next') - if current_user = render "layouts/nav/dashboard" diff --git a/app/views/layouts/header/_help_dropdown.html.haml b/app/views/layouts/header/_help_dropdown.html.haml index fbec62b02f8..5643a508ddc 100644 --- a/app/views/layouts/header/_help_dropdown.html.haml +++ b/app/views/layouts/header/_help_dropdown.html.haml @@ -8,6 +8,7 @@ = link_to _("Submit feedback"), "https://about.gitlab.com/submit-feedback" - if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile) = render 'shared/user_dropdown_contributing_link' + = render_if_exists 'shared/user_dropdown_instance_review' - if Gitlab.com? %li.js-canary-link = link_to _("Switch to GitLab Next"), "https://next.gitlab.com/" diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb index f8032f3262b..1a06ea68bcd 100644 --- a/app/views/layouts/mailer.text.erb +++ b/app/views/layouts/mailer.text.erb @@ -4,5 +4,6 @@ -- <%# signature marker %> <%= _("You're receiving this email because of your account on %{host}.") % { host: Gitlab.config.gitlab.host } %> +<%= render_if_exists 'layouts/mailer/additional_text' %> <%= text_footer_message %> diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 5a27237bf76..47710b9e9e5 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -95,3 +95,4 @@ = link_to sherlock_transactions_path, class: 'admin-icon d-none d-lg-block d-xl-block', title: _('Sherlock Transactions'), data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('tachometer fw') + = render_if_exists 'layouts/nav/geo_primary_node_url' diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml index ece66d3180b..83fe871285a 100644 --- a/app/views/layouts/nav/sidebar/_admin.html.haml +++ b/app/views/layouts/nav/sidebar/_admin.html.haml @@ -48,7 +48,7 @@ %span = _('Gitaly Servers') - = nav_link(controller: %w(system_info background_jobs logs health_check requests_profiles)) do + = nav_link(controller: admin_monitoring_nav_links) do = link_to admin_system_info_path do .nav-icon-container = sprite_icon('monitor') @@ -81,6 +81,7 @@ = link_to admin_requests_profiles_path, title: _('Requests Profiles') do %span = _('Requests Profiles') + = render_if_exists 'layouts/nav/ee/admin/new_monitoring_sidebar' = nav_link(controller: :broadcast_messages) do = link_to admin_broadcast_messages_path do @@ -132,6 +133,21 @@ = _('Abuse Reports') %span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(AbuseReport.count(:all)) + = render_if_exists 'layouts/nav/sidebar/licenses_link' + + - if instance_clusters_enabled? + = nav_link(controller: :clusters) do + = link_to admin_clusters_path do + .nav-icon-container + = sprite_icon('cloud-gear') + %span.nav-item-name + = _('Kubernetes') + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: :clusters, html_options: { class: "fly-out-top-item" } ) do + = link_to admin_clusters_path do + %strong.fly-out-top-item-name + = _('Kubernetes') + - if akismet_enabled? = nav_link(controller: :spam_logs) do = link_to admin_spam_logs_path do @@ -145,6 +161,10 @@ %strong.fly-out-top-item-name = _('Spam Logs') + = render_if_exists 'layouts/nav/sidebar/push_rules_link' + + = render_if_exists 'layouts/nav/ee/admin/geo_sidebar' + = nav_link(controller: :deploy_keys) do = link_to admin_deploy_keys_path do .nav-icon-container diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index b950e53639a..0fc5ebbea7e 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -1,6 +1,5 @@ - issues_count = group_issues_count(state: 'opened') - merge_requests_count = group_merge_requests_count(state: 'opened') -- issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index', 'boards#index', 'boards#show'] .nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) } .nav-sidebar-inner-scroll @@ -46,11 +45,12 @@ = _('Contribution Analytics') = render_if_exists 'layouts/nav/group_insights_link' + = render_if_exists 'groups/sidebar/dependency_proxy' # EE-specific = render_if_exists "layouts/nav/ee/epic_link", group: @group - if group_sidebar_link?(:issues) - = nav_link(path: issues_sub_menu_items) do + = nav_link(path: group_issues_sub_menu_items) do = link_to issues_group_path(@group) do .nav-icon-container = sprite_icon('issues') diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml index 1e3bb8f1224..7dd33f3c641 100644 --- a/app/views/layouts/nav/sidebar/_profile.html.haml +++ b/app/views/layouts/nav/sidebar/_profile.html.haml @@ -4,7 +4,7 @@ = link_to profile_path, title: _('Profile Settings') do .avatar-container.s40.settings-avatar = image_tag avatar_icon_for_user(current_user, 40), class: "avatar s40 avatar-tile", alt: current_user.name - .sidebar-context-title User Settings + .sidebar-context-title= _('User Settings') %ul.sidebar-top-level-items = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do = link_to profile_path do @@ -28,6 +28,8 @@ = link_to profile_account_path do %strong.fly-out-top-item-name = _('Account') + + = render_if_exists 'layouts/nav/sidebar/profile_billing_link' = nav_link(controller: 'oauth/applications') do = link_to applications_profile_path do .nav-icon-container @@ -151,4 +153,6 @@ %strong.fly-out-top-item-name = _('Authentication Log') + = render_if_exists 'layouts/nav/sidebar/profile_pipeline_quota_link' + = render 'shared/sidebar_toggle_button' diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index 3a0c2b9c284..399305baec1 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -270,6 +270,8 @@ %span= _("Got it!") = sprite_icon('thumb-up') + = render_if_exists 'layouts/nav/sidebar/project_feature_flags_link' + - if project_nav_tab? :container_registry = nav_link(controller: %w[projects/registry/repositories]) do = link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry' do @@ -283,7 +285,9 @@ %strong.fly-out-top-item-name = _('Registry') - - if project_nav_tab?(:wiki) + = render_if_exists 'layouts/nav/sidebar/project_packages_link' + + - if project_nav_tab? :wiki - wiki_url = project_wiki_path(@project, :home) = nav_link(controller: :wikis) do = link_to wiki_url, class: 'shortcuts-wiki qa-wiki-link' do diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml index 8dff12c1b7f..de487a94d40 100644 --- a/app/views/layouts/notify.html.haml +++ b/app/views/layouts/notify.html.haml @@ -31,4 +31,6 @@ adjust your notification settings. = email_action @target_url + + = render_if_exists 'layouts/email_additional_text' = html_footer_message diff --git a/app/views/layouts/notify.text.erb b/app/views/layouts/notify.text.erb index 248916fba63..0ee30c2a6cf 100644 --- a/app/views/layouts/notify.text.erb +++ b/app/views/layouts/notify.text.erb @@ -12,5 +12,6 @@ <% end -%> <%= "You're receiving this email because #{notification_reason_text(@reason)}." %> +<%= render_if_exists 'layouts/mailer/additional_text' %> <%= text_footer_message -%> diff --git a/app/views/notify/closed_issue_email.html.haml b/app/views/notify/closed_issue_email.html.haml index eb148d72da1..f21cf1ad34b 100644 --- a/app/views/notify/closed_issue_email.html.haml +++ b/app/views/notify/closed_issue_email.html.haml @@ -1,2 +1,2 @@ %p - Issue was closed by #{sanitize_name(@updated_by.name)} + Issue was closed by #{sanitize_name(@updated_by.name)} #{closure_reason_text(@closed_via, format: formats.first)}. diff --git a/app/views/notify/closed_issue_email.text.haml b/app/views/notify/closed_issue_email.text.haml index b1f0a3f37ec..5567adc9165 100644 --- a/app/views/notify/closed_issue_email.text.haml +++ b/app/views/notify/closed_issue_email.text.haml @@ -1,3 +1,3 @@ -Issue was closed by #{sanitize_name(@updated_by.name)} +Issue was closed by #{sanitize_name(@updated_by.name)} #{closure_reason_text(@closed_via, format: formats.first)}. Issue ##{@issue.iid}: #{project_issue_url(@issue.project, @issue)} diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index bfe1c3ddf33..58f2eb229ba 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -82,5 +82,31 @@ = f.label :first_day_of_week, class: 'label-bold' do = _('First day of the week') = f.select :first_day_of_week, first_day_of_week_choices_with_default, {}, class: 'form-control' + - if Feature.enabled?(:user_time_settings) + .col-sm-12 + %hr + .col-lg-4.profile-settings-sidebar + %h4.prepend-top-0= s_('Preferences|Time preferences') + %p= s_('Preferences|These settings will update how dates and times are displayed for you.') + .col-lg-8 + .form-group + %h5= s_('Preferences|Time format') + .checkbox-icon-inline-wrapper.form-check + - time_format_label = capture do + = s_('Preferences|Display time in 24-hour format') + = f.check_box :time_format_in_24h, class: 'form-check-input' + = f.label :time_format_in_24h do + = time_format_label + %h5= s_('Preferences|Time display') + .checkbox-icon-inline-wrapper.form-check + - time_display_label = capture do + = s_('Preferences|Use relative times') + = f.check_box :time_display_relative, class: 'form-check-input' + = f.label :time_display_relative do + = time_display_label + .text-muted + = s_('Preferences|For example: 30 mins ago.') + .col-lg-4.profile-settings-sidebar + .col-lg-8 .form-group = f.submit _('Save changes'), class: 'btn btn-success' diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 02c750a92c3..917e7acc353 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -64,6 +64,18 @@ prepend: emoji_button, append: reset_message_button, placeholder: s_("Profiles|What's your status?") + - if Feature.enabled?(:user_time_settings) + %hr + .row.user-time-preferences + .col-lg-4.profile-settings-sidebar + %h4.prepend-top-0= s_("Profiles|Time settings") + %p= s_("Profiles|You can set your current timezone here") + .col-lg-8 + -# TODO: might need an entry in user/profile.md to describe some of these settings + -# https://gitlab.com/gitlab-org/gitlab-ce/issues/60070 + %h5= ("Time zone") + = dropdown_tag(_("Select a timezone"), options: { toggle_class: 'btn js-timezone-dropdown input-lg', title: _("Select a timezone"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: timezone_data } } ) + %input.hidden{ :type => 'hidden', :id => 'user_timezone', :name => 'user[timezone]', value: @user.timezone } %hr .row @@ -80,8 +92,8 @@ = f.text_field :name, required: true, readonly: true, wrapper: { class: 'col-md-9 qa-full-name' }, help: s_("Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you") % { provider_label: attribute_provider_label(:name) } - else - = f.text_field :name, label: 'Full name', required: true, title: s_("Profiles|Using emojis in names seems fun, but please try to set a status message instead"), wrapper: { class: 'col-md-9 qa-full-name' }, help: s_("Profiles|Enter your name, so people you know can recognize you") - = f.text_field :id, readonly: true, label: 'User ID', wrapper: { class: 'col-md-3' } + = f.text_field :name, label: s_('Profiles|Full name'), required: true, title: s_("Profiles|Using emojis in names seems fun, but please try to set a status message instead"), wrapper: { class: 'col-md-9 qa-full-name' }, help: s_("Profiles|Enter your name, so people you know can recognize you") + = f.text_field :id, readonly: true, label: s_('Profiles|User ID'), wrapper: { class: 'col-md-3' } = render_if_exists 'profiles/email_settings', form: f = f.text_field :skype, class: 'input-md', placeholder: s_("Profiles|username") @@ -91,18 +103,18 @@ - if @user.read_only_attribute?(:location) = f.text_field :location, readonly: true, help: s_("Profiles|Your location was automatically set based on your %{provider_label} account") % { provider_label: attribute_provider_label(:location) } - else - = f.text_field :location, class: 'input-lg', placeholder: s_("Profiles|City, country") - = f.text_field :organization, class: 'input-md', help: s_("Profiles|Who you represent or work for") - = f.text_area :bio, rows: 4, maxlength: 250, help: s_("Profiles|Tell us about yourself in fewer than 250 characters") + = f.text_field :location, label: s_('Profiles|Location'), class: 'input-lg', placeholder: s_("Profiles|City, country") + = f.text_field :organization, label: s_('Profiles|Organization'), class: 'input-md', help: s_("Profiles|Who you represent or work for") + = f.text_area :bio, label: s_('Profiles|Bio'), rows: 4, maxlength: 250, help: s_("Profiles|Tell us about yourself in fewer than 250 characters") %hr - %h5= ("Private profile") + %h5= s_("Private profile") .checkbox-icon-inline-wrapper - private_profile_label = capture do = s_("Profiles|Don't display activity-related personal information on your profiles") = f.check_box :private_profile, label: private_profile_label, inline: true, wrapper_class: 'mr-0' = link_to icon('question-circle'), help_page_path('user/profile/index.md', anchor: 'private-profile') %h5= s_("Profiles|Private contributions") - = f.check_box :include_private_contributions, label: 'Include private contributions on my profile', wrapper_class: 'mb-2', inline: true + = f.check_box :include_private_contributions, label: s_('Profiles|Include private contributions on my profile'), wrapper_class: 'mb-2', inline: true .help-block = s_("Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information") .prepend-top-default.append-bottom-default diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml index 22a721ee9ad..0edd8ee5e46 100644 --- a/app/views/projects/_files.html.haml +++ b/app/views/projects/_files.html.haml @@ -4,6 +4,7 @@ - project = local_assigns.fetch(:project) { @project } - content_url = local_assigns.fetch(:content_url) { @tree.readme ? project_blob_path(@project, tree_join(@ref, @tree.readme.path)) : project_tree_path(@project, @ref) } - show_auto_devops_callout = show_auto_devops_callout?(@project) +- vue_file_list = Feature.enabled?(:vue_file_list, @project) #tree-holder.tree-holder.clearfix .nav-block @@ -13,7 +14,12 @@ = render 'shared/commit_well', commit: commit, ref: ref, project: project - if is_project_overview - .project-buttons.append-bottom-default + .project-buttons.append-bottom-default{ class: ("js-hide-on-navigation" if vue_file_list) } = render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout) - = render 'projects/tree/tree_content', tree: @tree, content_url: content_url + - if vue_file_list + #js-tree-list{ data: { project_path: @project.full_path, ref: ref } } + - if @tree.readme + = render "projects/tree/readme", readme: @tree.readme + - else + = render 'projects/tree/tree_content', tree: @tree, content_url: content_url diff --git a/app/views/projects/_flash_messages.html.haml b/app/views/projects/_flash_messages.html.haml index b72f0e39b23..b2dab0b5348 100644 --- a/app/views/projects/_flash_messages.html.haml +++ b/app/views/projects/_flash_messages.html.haml @@ -7,3 +7,4 @@ = render 'shared/no_password' - unless project.empty_repo? = render 'shared/auto_devops_implicitly_enabled_banner', project: project + = render_if_exists 'projects/above_size_limit_warning', project: project diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 3ca4abddbb8..a97322dace4 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,7 +1,7 @@ - empty_repo = @project.empty_repo? - show_auto_devops_callout = show_auto_devops_callout?(@project) - max_project_topic_length = 15 -.project-home-panel{ class: ("empty-project" if empty_repo) } +.project-home-panel{ class: [("empty-project" if empty_repo), ("js-hide-on-navigation" if Feature.enabled?(:vue_file_list, @project))] } .row.append-bottom-8 .home-panel-title-row.col-md-12.col-lg-6.d-flex .avatar-container.rect-avatar.s64.home-panel-avatar.append-right-default.float-none diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 91c51d5e091..0e15f581ddc 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -10,7 +10,7 @@ .branch-info .branch-title = sprite_icon('fork', size: 12) - = link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated-100 ref-name prepend-left-8' do + = link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated-100 ref-name prepend-left-8 qa-branch-name' do = branch.name - if branch.name == @repository.root_ref %span.badge.badge-primary.prepend-left-5 default diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 43f1cd01b67..d270e461ac8 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -1,5 +1,6 @@ - @no_container = true - page_title _('Branches') +- add_to_breadcrumbs(_('Repository'), project_tree_path(@project)) %div{ class: container_class } .top-area.adjust @@ -44,6 +45,8 @@ = link_to new_project_branch_path(@project), class: 'btn btn-success' do = s_('Branches|New branch') + = render_if_exists 'projects/commits/mirror_status' + - if can?(current_user, :admin_project, @project) - project_settings_link = link_to s_('Branches|project settings'), project_protected_branches_path(@project) .row-content-block diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index 9d254463fb6..2db1efdd52f 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -30,6 +30,8 @@ = link_to project_commits_path(@project, @ref, rss_url_options), title: _("Commits feed"), class: 'btn' do = icon("rss") + = render_if_exists 'projects/commits/mirror_status' + %div{ id: dom_id(@project) } %ol#commits-list.list-unstyled.content_list = render 'commits', project: @project, ref: @ref diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 715c36fa9aa..d55afee4523 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -79,7 +79,7 @@ = render_if_exists 'projects/issues/related_issues' - #js-related-merge-requests{ data: { endpoint: expose_url(api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: @issue.iid)), project_namespace: @project.namespace.path, project_path: @project.path } } + #js-related-merge-requests{ data: { endpoint: expose_path(api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: @issue.iid)), project_namespace: @project.namespace.path, project_path: @project.path } } - if can?(current_user, :download_code, @project) #related-branches{ data: { url: related_branches_project_issue_path(@project, @issue) } } diff --git a/app/views/projects/mirrors/_disabled_mirror_badge.html.haml b/app/views/projects/mirrors/_disabled_mirror_badge.html.haml new file mode 100644 index 00000000000..356cb43f07f --- /dev/null +++ b/app/views/projects/mirrors/_disabled_mirror_badge.html.haml @@ -0,0 +1 @@ +.badge.badge-warning.qa-disabled-mirror-badge{ data: { toggle: 'tooltip', html: 'true' }, title: _('Disabled mirrors can only be enabled by instance owners. It is recommended that you delete them.') }= _('Disabled') diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml index 0cd00d3e708..e68fa5d08c7 100644 --- a/app/views/projects/mirrors/_mirror_repos.html.haml +++ b/app/views/projects/mirrors/_mirror_repos.html.haml @@ -29,7 +29,7 @@ .form-check.append-bottom-10 = check_box_tag :only_protected_branches, '1', false, class: 'js-mirror-protected form-check-input' = label_tag :only_protected_branches, _('Only mirror protected branches'), class: 'form-check-label' - = link_to icon('question-circle'), help_page_path('user/project/protected_branches') + = link_to icon('question-circle'), help_page_path('user/project/protected_branches'), target: '_blank' .panel-footer = f.submit _('Mirror repository'), class: 'btn btn-success js-mirror-submit qa-mirror-repository-button', name: :update_remote_mirror @@ -49,17 +49,19 @@ %tbody.js-mirrors-table-body = render_if_exists 'projects/mirrors/table_pull_row' - @project.remote_mirrors.each_with_index do |mirror, index| - - if mirror.enabled - %tr.qa-mirrored-repository-row - %td.qa-mirror-repository-url= mirror.safe_url - %td= _('Push') - %td.qa-mirror-last-update-at= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never') - %td - - if mirror.last_error.present? - .badge.mirror-error-badge{ data: { toggle: 'tooltip', html: 'true' }, title: html_escape(mirror.last_error.try(:strip)) }= _('Error') - %td - .btn-group.mirror-actions-group.pull-right{ role: 'group' } - - if mirror.ssh_key_auth? - = clipboard_button(text: mirror.ssh_public_key, class: 'btn btn-default', title: _('Copy SSH public key')) - = render 'shared/remote_mirror_update_button', remote_mirror: mirror - %button.js-delete-mirror.btn.btn-danger{ type: 'button', data: { mirror_id: mirror.id, toggle: 'tooltip', container: 'body' }, title: _('Remove') }= icon('trash-o') + - next if mirror.new_record? + %tr.qa-mirrored-repository-row{ class: ('bg-secondary' if mirror.disabled?) } + %td.qa-mirror-repository-url= mirror.safe_url + %td= _('Push') + %td.qa-mirror-last-update-at= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never') + %td + - if mirror.disabled? + = render 'projects/mirrors/disabled_mirror_badge' + - if mirror.last_error.present? + .badge.mirror-error-badge{ data: { toggle: 'tooltip', html: 'true' }, title: html_escape(mirror.last_error.try(:strip)) }= _('Error') + %td + .btn-group.mirror-actions-group.pull-right{ role: 'group' } + - if mirror.ssh_key_auth? + = clipboard_button(text: mirror.ssh_public_key, class: 'btn btn-default', title: _('Copy SSH public key')) + = render 'shared/remote_mirror_update_button', remote_mirror: mirror + %button.js-delete-mirror.qa-delete-mirror.btn.btn-danger{ type: 'button', data: { mirror_id: mirror.id, toggle: 'tooltip', container: 'body' }, title: _('Remove') }= icon('trash-o') diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml index 0590578c3fe..efabb7f7b19 100644 --- a/app/views/projects/project_members/_new_project_member.html.haml +++ b/app/views/projects/project_members/_new_project_member.html.haml @@ -19,4 +19,5 @@ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date' %i.clear-icon.js-clear-input = f.submit _("Add to project"), class: "btn btn-success qa-add-member-button" - = link_to _("Import"), import_project_project_members_path(@project), class: "btn btn-default", title: _("Import members from another project") + - if can_import_members? + = link_to _("Import"), import_project_project_members_path(@project), class: "btn btn-default", title: _("Import members from another project") diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml index b12ae995ece..366d7a7a2eb 100644 --- a/app/views/projects/protected_branches/_protected_branch.html.haml +++ b/app/views/projects/protected_branches/_protected_branch.html.haml @@ -1,2 +1,2 @@ = render layout: 'projects/protected_branches/shared/protected_branch', locals: { protected_branch: protected_branch } do - = render partial: 'projects/protected_branches/update_protected_branch', locals: { protected_branch: protected_branch } + = render_if_exists 'projects/protected_branches/update_protected_branch', protected_branch: protected_branch diff --git a/app/views/projects/settings/_general.html.haml b/app/views/projects/settings/_general.html.haml index 380430ff52b..520f342f567 100644 --- a/app/views/projects/settings/_general.html.haml +++ b/app/views/projects/settings/_general.html.haml @@ -27,7 +27,7 @@ .row= render_if_exists 'projects/classification_policy_settings', f: f - .row= render_if_exists 'shared/repository_size_limit_setting', form: f, type: :project + = render_if_exists 'shared/repository_size_limit_setting', form: f, type: :project .form-group.prepend-top-default.append-bottom-20 .avatar-container.s90 diff --git a/app/views/projects/settings/operations/_external_dashboard.html.haml b/app/views/projects/settings/operations/_external_dashboard.html.haml new file mode 100644 index 00000000000..2fbb9195a04 --- /dev/null +++ b/app/views/projects/settings/operations/_external_dashboard.html.haml @@ -0,0 +1,2 @@ +.js-operation-settings{ data: { external_dashboard: { path: '', + help_page_path: help_page_path('user/project/operations/link_to_external_dashboard') } } } diff --git a/app/views/projects/settings/operations/show.html.haml b/app/views/projects/settings/operations/show.html.haml index 6f777305a54..edc2c58a8ed 100644 --- a/app/views/projects/settings/operations/show.html.haml +++ b/app/views/projects/settings/operations/show.html.haml @@ -4,4 +4,5 @@ = render_if_exists 'projects/settings/operations/incidents' = render 'projects/settings/operations/error_tracking', expanded: true += render 'projects/settings/operations/external_dashboard' = render_if_exists 'projects/settings/operations/tracing' diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml index 4daacbe157c..e935af23659 100644 --- a/app/views/projects/tree/_readme.html.haml +++ b/app/views/projects/tree/_readme.html.haml @@ -1,5 +1,5 @@ - if readme.rich_viewer - %article.file-holder.readme-holder{ id: 'readme', class: ("limited-width-container" unless fluid_layout) } + %article.file-holder.readme-holder{ id: 'readme', class: [("limited-width-container" unless fluid_layout), ("js-hide-on-navigation" if Feature.enabled?(:vue_file_list, @project))] } .js-file-title.file-title = blob_icon readme.mode, readme.name = link_to project_blob_path(@project, tree_join(@ref, readme.path)) do diff --git a/app/views/repository_check_mailer/notify.text.haml b/app/views/repository_check_mailer/notify.text.haml index 6b64b337b0e..a2e04fa710f 100644 --- a/app/views/repository_check_mailer/notify.text.haml +++ b/app/views/repository_check_mailer/notify.text.haml @@ -3,3 +3,5 @@ = _("View details: %{details_url}") % { details_url: admin_projects_url(last_repository_check_failed: 1) } = _("You are receiving this message because you are a GitLab administrator for %{url}.") % { url: Gitlab.config.gitlab.url } + += render_if_exists 'repository_check_mailer/email_additional_text' diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index a2df0347fd6..1e509ea0d1f 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -16,7 +16,12 @@ = ssh_clone_button(project) %li = http_clone_button(project) + = render_if_exists 'shared/kerberos_clone_button', project: project = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true, aria: { label: 'Project clone URL' } .input-group-append = clipboard_button(target: '#project_clone', title: _("Copy URL to clipboard"), class: "input-group-text btn-default btn-clipboard") + + = render_if_exists 'shared/geo_modal_button' + += render_if_exists 'shared/geo_modal', project: project diff --git a/app/views/shared/_remote_mirror_update_button.html.haml b/app/views/shared/_remote_mirror_update_button.html.haml index 721a2af8069..8da2ae5111a 100644 --- a/app/views/shared/_remote_mirror_update_button.html.haml +++ b/app/views/shared/_remote_mirror_update_button.html.haml @@ -1,6 +1,6 @@ - if remote_mirror.update_in_progress? %button.btn.disabled{ type: 'button', data: { toggle: 'tooltip', container: 'body' }, title: _('Updating') } = icon("refresh spin") -- else +- elsif remote_mirror.enabled? = link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn qa-update-now-button", data: { toggle: 'tooltip', container: 'body' }, title: _('Update now') do = icon("refresh") diff --git a/app/views/shared/groups/_dropdown.html.haml b/app/views/shared/groups/_dropdown.html.haml index 1ae6d1f5ee3..f4915440cb2 100644 --- a/app/views/shared/groups/_dropdown.html.haml +++ b/app/views/shared/groups/_dropdown.html.haml @@ -24,10 +24,10 @@ %li.divider %li.js-filter-archived-projects = link_to filter_groups_path(archived: nil), class: ("is-active" unless params[:archived].present?) do - Hide archived projects + = _("Hide archived projects") %li.js-filter-archived-projects = link_to filter_groups_path(archived: true), class: ("is-active" if Gitlab::Utils.to_boolean(params[:archived])) do - Show archived projects + = _("Show archived projects") %li.js-filter-archived-projects = link_to filter_groups_path(archived: 'only'), class: ("is-active" if params[:archived] == 'only') do - Show archived projects only + = _("Show archived projects only") diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 2c185549b24..63557c882f4 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -164,7 +164,7 @@ = dropdown_content = dropdown_loading = dropdown_footer add_content_class: true do - %button.btn.btn-success.sidebar-move-issue-confirmation-button.js-move-issue-confirmation-button{ disabled: true } + %button.btn.btn-success.sidebar-move-issue-confirmation-button.js-move-issue-confirmation-button{ type: 'button', disabled: true } = _('Move') = icon('spinner spin', class: 'sidebar-move-issue-confirmation-loading-icon') diff --git a/app/views/shared/issuable/form/_contribution.html.haml b/app/views/shared/issuable/form/_contribution.html.haml index bc9a1edc39c..a78231b37ce 100644 --- a/app/views/shared/issuable/form/_contribution.html.haml +++ b/app/views/shared/issuable/form/_contribution.html.haml @@ -15,6 +15,6 @@ = form.check_box :allow_collaboration, disabled: !issuable.can_allow_collaboration?(current_user), class: 'form-check-input' = form.label :allow_collaboration, class: 'form-check-label' do = _('Allow commits from members who can merge to the target branch.') - = link_to 'About this feature', help_page_path('user/project/merge_requests/allow_collaboration') + = link_to 'About this feature', help_page_path('user/project/merge_requests/allow_collaboration'), target: '_blank', rel: 'noopener noreferrer nofollow' .form-text.text-muted = allow_collaboration_unavailable_reason(issuable) diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml index 98b258d9275..88ac03bf9e3 100644 --- a/app/views/shared/projects/_dropdown.html.haml +++ b/app/views/shared/projects/_dropdown.html.haml @@ -1,10 +1,9 @@ - @sort ||= sort_value_latest_activity .dropdown.js-project-filter-dropdown-wrap - - toggle_text = projects_sort_options_hash[@sort] - = dropdown_toggle(toggle_text, { toggle: 'dropdown', display: 'static' }, { id: 'sort-projects-dropdown' }) + = dropdown_toggle(projects_sort_options_hash[@sort], { toggle: 'dropdown', display: 'static' }, { id: 'sort-projects-dropdown' }) %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable %li.dropdown-header - Sort by + = _("Sort by") - projects_sort_options_hash.each do |value, title| %li = link_to filter_projects_path(sort: value), class: ("is-active" if @sort == value) do @@ -13,29 +12,29 @@ %li.divider %li = link_to filter_projects_path(archived: nil), class: ("is-active" unless params[:archived].present?) do - Hide archived projects + = _("Hide archived projects") %li = link_to filter_projects_path(archived: true), class: ("is-active" if Gitlab::Utils.to_boolean(params[:archived])) do - Show archived projects + = _("Show archived projects") %li = link_to filter_projects_path(archived: 'only'), class: ("is-active" if params[:archived] == 'only') do - Show archived projects only + = _("Show archived projects only") - if current_user %li.divider %li = link_to filter_projects_path(personal: nil), class: ("is-active" unless params[:personal].present?) do - Owned by anyone + = _("Owned by anyone") %li = link_to filter_projects_path(personal: true), class: ("is-active" if params[:personal].present?) do - Owned by me + = _("Owned by me") - if @group && @group.shared_projects.present? %li.divider %li = link_to filter_projects_path(shared: nil), class: ("is-active" unless params[:shared].present?) do - All projects + = _("All projects") %li = link_to filter_projects_path(shared: 0), class: ("is-active" if params[:shared] == '0') do - Hide shared projects + = _("Hide shared projects") %li = link_to filter_projects_path(shared: 1), class: ("is-active" if params[:shared] == '1') do - Hide group projects + = _("Hide group projects") diff --git a/app/views/shared/projects/_search_bar.html.haml b/app/views/shared/projects/_search_bar.html.haml new file mode 100644 index 00000000000..c1f2eaba284 --- /dev/null +++ b/app/views/shared/projects/_search_bar.html.haml @@ -0,0 +1,28 @@ +- @sort ||= sort_value_latest_activity +- project_tab_filter = local_assigns.fetch(:project_tab_filter, "") +- flex_grow_and_shrink_xs = 'd-flex flex-xs-grow-1 flex-xs-shrink-1 flex-grow-0 flex-shrink-0' + +.filtered-search-block.row-content-block.bt-0 + .filtered-search-wrapper.d-flex.flex-nowrap.flex-column.flex-sm-wrap.flex-sm-row.flex-xl-nowrap + - unless project_tab_filter == :starred + .filtered-search-nav.mb-2.mb-lg-0{ class: flex_grow_and_shrink_xs } + = render 'dashboard/projects/nav', project_tab_filter: project_tab_filter + .filtered-search.d-flex.flex-grow-1.flex-shrink-1.w-100.mb-2.mb-lg-0.ml-0{ class: project_tab_filter == :starred ? "extended-filtered-search-box mb-2 mb-lg-0" : "ml-sm-3" } + .btn-group.w-100{ role: "group" } + .btn-group.w-100{ role: "group" } + .filtered-search-box.m-0 + .filtered-search-box-input-container.pl-2 + = render 'shared/projects/search_form', admin_view: false, search_form_placeholder: _("Search projects...") + %button.btn.btn-secondary{ type: 'submit', form: 'project-filter-form' } + = sprite_icon('search', size: 16, css_class: 'search-icon ') + .filtered-search-dropdown.flex-row.align-items-center.mb-2.m-sm-0#filtered-search-visibility-dropdown{ class: flex_grow_and_shrink_xs } + .filtered-search-dropdown-label.p-0.pl-sm-3.font-weight-bold + %span + = _("Visibility") + = render 'explore/projects/filter', has_label: true + .filtered-search-dropdown.flex-row.align-items-center.m-sm-0#filtered-search-sorting-dropdown{ class: flex_grow_and_shrink_xs } + .filtered-search-dropdown-label.p-0.pl-sm-3.font-weight-bold + %span + = _("Sort by") + = render 'shared/projects/sort_dropdown' + diff --git a/app/views/shared/projects/_search_form.html.haml b/app/views/shared/projects/_search_form.html.haml index 3b5c13ed93a..7c7c0a363ac 100644 --- a/app/views/shared/projects/_search_form.html.haml +++ b/app/views/shared/projects/_search_form.html.haml @@ -1,7 +1,10 @@ +- form_field_classes = local_assigns[:admin_view] || !Feature.enabled?(:project_list_filter_bar) ? 'input-short js-projects-list-filter' : '' +- placeholder = local_assigns[:search_form_placeholder] ? search_form_placeholder : 'Filter by name...' + = form_tag filter_projects_path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| = search_field_tag :name, params[:name], - placeholder: 'Filter by name...', - class: 'project-filter-form-field form-control input-short js-projects-list-filter', + placeholder: placeholder, + class: "project-filter-form-field form-control #{form_field_classes}", spellcheck: false, id: 'project-filter-form-field', tabindex: "2", diff --git a/app/views/shared/projects/_sort_dropdown.html.haml b/app/views/shared/projects/_sort_dropdown.html.haml new file mode 100644 index 00000000000..f5f940db189 --- /dev/null +++ b/app/views/shared/projects/_sort_dropdown.html.haml @@ -0,0 +1,39 @@ +- @sort ||= sort_value_latest_activity +- toggle_text = projects_sort_option_titles[@sort] + +.btn-group.w-100{ role: "group" } + .btn-group.w-100.dropdown.js-project-filter-dropdown-wrap{ role: "group" } + %button#sort-projects-dropdown.btn.btn-default.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' } } + = toggle_text + = icon('chevron-down') + %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable + %li.dropdown-header + = _("Sort by") + - projects_sort_options_hash.each do |value, title| + %li + = link_to title, filter_projects_path(sort: value), class: ("is-active" if toggle_text == title) + + %li.divider + %li + = link_to filter_projects_path(archived: nil), class: ("is-active" unless params[:archived].present?) do + = _("Hide archived projects") + %li + = link_to filter_projects_path(archived: true), class: ("is-active" if Gitlab::Utils.to_boolean(params[:archived])) do + = _("Show archived projects") + %li + = link_to filter_projects_path(archived: 'only'), class: ("is-active" if params[:archived] == 'only') do + = _("Show archived projects only") + + - if current_user && @group && @group.shared_projects.present? + %li.divider + %li + = link_to filter_projects_path(shared: nil), class: ("is-active" unless params[:shared].present?) do + = _("All projects") + %li + = link_to filter_projects_path(shared: 0), class: ("is-active" if params[:shared] == '0') do + = _("Hide shared projects") + %li + = link_to filter_projects_path(shared: 1), class: ("is-active" if params[:shared] == '1') do + = _("Hide group projects") + + = project_sort_direction_button(@sort) diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml index 01acbf8eadd..3191eaa1e2c 100644 --- a/app/views/users/calendar_activities.html.haml +++ b/app/views/users/calendar_activities.html.haml @@ -9,7 +9,7 @@ %i.fa.fa-clock-o = event.created_at.to_time.in_time_zone.strftime('%-I:%M%P') - if event.visible_to_user?(current_user) - - if event.push? + - if event.push_action? #{event.action_name} #{event.ref_type} %strong - commits_path = project_commits_path(event.project, event.ref_name) diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 211e3eafac6..6dc61088e65 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -45,7 +45,7 @@ = emoji_icon(@user.status.emoji) = markdown_field(@user.status, :message) - .cover-desc.member-date + .cover-desc.member-date.cgray %p %span.middle-dot-divider @#{@user.username} @@ -53,7 +53,7 @@ %span.middle-dot-divider = s_('Member since %{date}') % { date: @user.created_at.to_date.to_s(:long) } - .cover-desc + .cover-desc.cgray - unless @user.public_email.blank? .profile-link-holder.middle-dot-divider = link_to @user.public_email, "mailto:#{@user.public_email}", class: 'text-link' @@ -82,7 +82,7 @@ = @user.organization - if @user.bio.present? - .cover-desc + .cover-desc.cgray %p.profile-user-bio = @user.bio diff --git a/app/workers/cluster_configure_worker.rb b/app/workers/cluster_configure_worker.rb index 37ea7dde7a1..6f64b7ea0ab 100644 --- a/app/workers/cluster_configure_worker.rb +++ b/app/workers/cluster_configure_worker.rb @@ -6,7 +6,7 @@ class ClusterConfigureWorker def perform(cluster_id) Clusters::Cluster.managed.find_by_id(cluster_id).try do |cluster| - if cluster.project_type? || Feature.disabled?(:ci_preparing_state, default_enabled: true) + if cluster.project_type? Clusters::RefreshService.create_or_update_namespaces_for_cluster(cluster) end end diff --git a/app/workers/detect_repository_languages_worker.rb b/app/workers/detect_repository_languages_worker.rb index 64bc9776d48..838c3be78f0 100644 --- a/app/workers/detect_repository_languages_worker.rb +++ b/app/workers/detect_repository_languages_worker.rb @@ -12,13 +12,12 @@ class DetectRepositoryLanguagesWorker attr_reader :project # rubocop: disable CodeReuse/ActiveRecord - def perform(project_id, user_id) + def perform(project_id, user_id = nil) @project = Project.find_by(id: project_id) - user = User.find_by(id: user_id) - return unless project && user + return unless project try_obtain_lease do - ::Projects::DetectRepositoryLanguagesService.new(project, user).execute + ::Projects::DetectRepositoryLanguagesService.new(project).execute end end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/workers/pages_domain_removal_cron_worker.rb b/app/workers/pages_domain_removal_cron_worker.rb index 3aca123e5ac..79f38e1b89f 100644 --- a/app/workers/pages_domain_removal_cron_worker.rb +++ b/app/workers/pages_domain_removal_cron_worker.rb @@ -5,8 +5,6 @@ class PagesDomainRemovalCronWorker include CronjobQueue def perform - return unless Feature.enabled?(:remove_disabled_domains) - PagesDomain.for_removal.find_each do |domain| domain.destroy! rescue => e diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb index 29a7f8e691a..3efb5343a96 100644 --- a/app/workers/process_commit_worker.rb +++ b/app/workers/process_commit_worker.rb @@ -48,7 +48,7 @@ class ProcessCommitWorker # Issues::CloseService#execute. IssueCollection.new(issues).updatable_by_user(user).each do |issue| Issues::CloseService.new(project, author) - .close_issue(issue, commit: commit) + .close_issue(issue, closed_via: commit) end end diff --git a/changelogs/unreleased/10808-allow-license-import-during-install.yml b/changelogs/unreleased/10808-allow-license-import-during-install.yml new file mode 100644 index 00000000000..f93edf03d51 --- /dev/null +++ b/changelogs/unreleased/10808-allow-license-import-during-install.yml @@ -0,0 +1,5 @@ +--- +title: Document EE License Auto Import During Install +merge_request: 28106 +author: +type: other diff --git a/changelogs/unreleased/1340-request-job-with-age.yml b/changelogs/unreleased/1340-request-job-with-age.yml new file mode 100644 index 00000000000..766ac008c2e --- /dev/null +++ b/changelogs/unreleased/1340-request-job-with-age.yml @@ -0,0 +1,5 @@ +--- +title: "Added option to filter jobs by age in the /job/request API endpoint." +merge_request: 1340 +author: Dmitry Chepurovskiy +type: added diff --git a/changelogs/unreleased/19569-include-information-if-issue-was-closed-via-mr.yml b/changelogs/unreleased/19569-include-information-if-issue-was-closed-via-mr.yml new file mode 100644 index 00000000000..bb2fc9af2a1 --- /dev/null +++ b/changelogs/unreleased/19569-include-information-if-issue-was-closed-via-mr.yml @@ -0,0 +1,5 @@ +--- +title: Include information if issue was clossed via merge request or commit +merge_request: 15610 +author: Michał Zając +type: changed diff --git a/changelogs/unreleased/237-style-toast-component.yml b/changelogs/unreleased/237-style-toast-component.yml new file mode 100644 index 00000000000..2420df0ee55 --- /dev/null +++ b/changelogs/unreleased/237-style-toast-component.yml @@ -0,0 +1,5 @@ +--- +title: Style the toast component according to design specs. +merge_request: 27734 +author: +type: added diff --git a/changelogs/unreleased/27987-use-findorcreateservice-to-create-labels.yml b/changelogs/unreleased/27987-use-findorcreateservice-to-create-labels.yml new file mode 100644 index 00000000000..8d3501e0171 --- /dev/null +++ b/changelogs/unreleased/27987-use-findorcreateservice-to-create-labels.yml @@ -0,0 +1,5 @@ +--- +title: Use FindOrCreateService to create labels and check for existing ones +merge_request: 27987 +author: Matt Duren +type: fixed diff --git a/changelogs/unreleased/28119-remove-note-multi-line-suggestions.yml b/changelogs/unreleased/28119-remove-note-multi-line-suggestions.yml new file mode 100644 index 00000000000..2fbacbcb011 --- /dev/null +++ b/changelogs/unreleased/28119-remove-note-multi-line-suggestions.yml @@ -0,0 +1,5 @@ +--- +title: Remove the note in the docs that multi-line suggestions are not yet available +merge_request: 28119 +author: hardysim +type: other diff --git a/changelogs/unreleased/49517-fix-notes-import-export.yml b/changelogs/unreleased/49517-fix-notes-import-export.yml new file mode 100644 index 00000000000..a9f4d736e0b --- /dev/null +++ b/changelogs/unreleased/49517-fix-notes-import-export.yml @@ -0,0 +1,5 @@ +--- +title: Fix diff notes and discussion notes being exported as regular notes +merge_request: 28401 +author: +type: fixed diff --git a/changelogs/unreleased/49915-fix-error-500-admin-projects-nil-storage.yml b/changelogs/unreleased/49915-fix-error-500-admin-projects-nil-storage.yml new file mode 100644 index 00000000000..307c2bfb49d --- /dev/null +++ b/changelogs/unreleased/49915-fix-error-500-admin-projects-nil-storage.yml @@ -0,0 +1,5 @@ +--- +title: Fix an error in projects admin when statistics are missing +merge_request: 28355 +author: +type: fixed diff --git a/changelogs/unreleased/53973-fix-subpixel-border-issue.yml b/changelogs/unreleased/53973-fix-subpixel-border-issue.yml new file mode 100644 index 00000000000..0dae7047236 --- /dev/null +++ b/changelogs/unreleased/53973-fix-subpixel-border-issue.yml @@ -0,0 +1,5 @@ +--- +title: Fix MR discussion border missing in chrome sometimes +merge_request: 28185 +author: +type: fixed diff --git a/changelogs/unreleased/54405-resolve-discussion-when-applying-a-suggested-change.yml b/changelogs/unreleased/54405-resolve-discussion-when-applying-a-suggested-change.yml new file mode 100644 index 00000000000..862ce623d8c --- /dev/null +++ b/changelogs/unreleased/54405-resolve-discussion-when-applying-a-suggested-change.yml @@ -0,0 +1,5 @@ +--- +title: Resolve discussion when apply suggestion +merge_request: 28160 +author: +type: changed diff --git a/changelogs/unreleased/55127-add-delay-after-mr-creation-for-async-tasks-to-complete.yml b/changelogs/unreleased/55127-add-delay-after-mr-creation-for-async-tasks-to-complete.yml new file mode 100644 index 00000000000..ac3bb596842 --- /dev/null +++ b/changelogs/unreleased/55127-add-delay-after-mr-creation-for-async-tasks-to-complete.yml @@ -0,0 +1,5 @@ +--- +title: Wait for pipeline creation to complete before accepting a MR via API +merge_request: 27978 +author: kerrizor +type: fixed diff --git a/changelogs/unreleased/57077-add-salesforce-omniauth.yml b/changelogs/unreleased/57077-add-salesforce-omniauth.yml new file mode 100644 index 00000000000..ebd0637ddac --- /dev/null +++ b/changelogs/unreleased/57077-add-salesforce-omniauth.yml @@ -0,0 +1,5 @@ +--- +title: Resolve Salesforce.com omniauth support +merge_request: 27834 +author: +type: added diff --git a/changelogs/unreleased/57654-add-time-preferences-for-user-fe.yml b/changelogs/unreleased/57654-add-time-preferences-for-user-fe.yml new file mode 100644 index 00000000000..f4ce3a51724 --- /dev/null +++ b/changelogs/unreleased/57654-add-time-preferences-for-user-fe.yml @@ -0,0 +1,5 @@ +--- +title: Add time preferences for user +merge_request: 25381 +author: +type: added diff --git a/changelogs/unreleased/58404-set-default-max-depth-for-GraphQL.yml b/changelogs/unreleased/58404-set-default-max-depth-for-GraphQL.yml new file mode 100644 index 00000000000..7e95158a0e0 --- /dev/null +++ b/changelogs/unreleased/58404-set-default-max-depth-for-GraphQL.yml @@ -0,0 +1,5 @@ +--- +title: 58404 - setup max depth for GraphQL +merge_request: 25737 +author: Ken Ding +type: added diff --git a/changelogs/unreleased/59105-padding-unclickable-pipeline-job.yml b/changelogs/unreleased/59105-padding-unclickable-pipeline-job.yml new file mode 100644 index 00000000000..95f08af3cb1 --- /dev/null +++ b/changelogs/unreleased/59105-padding-unclickable-pipeline-job.yml @@ -0,0 +1,5 @@ +--- +title: Fix padding of unclickable pipeline dropdown items to match links +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/60180-jira-service-fix-nil-on-find-call.yml b/changelogs/unreleased/60180-jira-service-fix-nil-on-find-call.yml new file mode 100644 index 00000000000..6891a9ca83c --- /dev/null +++ b/changelogs/unreleased/60180-jira-service-fix-nil-on-find-call.yml @@ -0,0 +1,5 @@ +--- +title: 'Resolved JIRA service: NoMethodError: undefined method ''find'' for nil:NilClass' +merge_request: 28206 +author: +type: fixed diff --git a/changelogs/unreleased/60379-remove-ci-preparing-state-feature-flag.yml b/changelogs/unreleased/60379-remove-ci-preparing-state-feature-flag.yml new file mode 100644 index 00000000000..a9b7aeb3024 --- /dev/null +++ b/changelogs/unreleased/60379-remove-ci-preparing-state-feature-flag.yml @@ -0,0 +1,5 @@ +--- +title: Remove ability for group clusters to be automatically configured on creation +merge_request: 27245 +author: +type: removed diff --git a/changelogs/unreleased/60425-fix-500-when-accessing-charts-with-anonymous-user.yml b/changelogs/unreleased/60425-fix-500-when-accessing-charts-with-anonymous-user.yml new file mode 100644 index 00000000000..4274dc5918c --- /dev/null +++ b/changelogs/unreleased/60425-fix-500-when-accessing-charts-with-anonymous-user.yml @@ -0,0 +1,5 @@ +--- +title: "Fix 500 error when accessing charts with an anonymous user" +merge_request: 28091 +author: Diego Silva +type: fixed diff --git a/changelogs/unreleased/60462-empty-pipeline-section.yml b/changelogs/unreleased/60462-empty-pipeline-section.yml new file mode 100644 index 00000000000..7d90215e20c --- /dev/null +++ b/changelogs/unreleased/60462-empty-pipeline-section.yml @@ -0,0 +1,5 @@ +--- +title: Fix empty block in MR widget when user doesn't have permission +merge_request: 27462 +author: +type: fixed diff --git a/changelogs/unreleased/60818_yamllint_project_root.yml b/changelogs/unreleased/60818_yamllint_project_root.yml new file mode 100644 index 00000000000..b34a50e6a9c --- /dev/null +++ b/changelogs/unreleased/60818_yamllint_project_root.yml @@ -0,0 +1,5 @@ +--- +title: Fix yaml linting for project root *.yml files +merge_request: 27579 +author: Will Hall +type: fixed diff --git a/changelogs/unreleased/61278-next.yml b/changelogs/unreleased/61278-next.yml new file mode 100644 index 00000000000..829f37f75ba --- /dev/null +++ b/changelogs/unreleased/61278-next.yml @@ -0,0 +1,5 @@ +--- +title: Render Next badge only for gitlab.com +merge_request: 28056 +author: +type: fixed diff --git a/changelogs/unreleased/61494-set-status-modal-visual-bugs.yml b/changelogs/unreleased/61494-set-status-modal-visual-bugs.yml new file mode 100644 index 00000000000..4126b8f93c1 --- /dev/null +++ b/changelogs/unreleased/61494-set-status-modal-visual-bugs.yml @@ -0,0 +1,5 @@ +--- +title: Fix visual issues in set status modal +merge_request: 28147 +author: +type: fixed diff --git a/changelogs/unreleased/61550-next-badge.yml b/changelogs/unreleased/61550-next-badge.yml new file mode 100644 index 00000000000..122e394a68c --- /dev/null +++ b/changelogs/unreleased/61550-next-badge.yml @@ -0,0 +1,5 @@ +--- +title: Fixes next badge being always visible +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/61606-support-string-piwik-website-ids.yml b/changelogs/unreleased/61606-support-string-piwik-website-ids.yml new file mode 100644 index 00000000000..5c525294132 --- /dev/null +++ b/changelogs/unreleased/61606-support-string-piwik-website-ids.yml @@ -0,0 +1,5 @@ +--- +title: "Supports Matomo/Piwik string website ID (\"Protect Track ID\" plugin)" +merge_request: 28214 +author: DUVERGIER Claude +type: fixed
\ No newline at end of file diff --git a/changelogs/unreleased/61629-dependency-installation-error-on-fsevents-1-2-4-with-node-js-12.yml b/changelogs/unreleased/61629-dependency-installation-error-on-fsevents-1-2-4-with-node-js-12.yml new file mode 100644 index 00000000000..bbe43760953 --- /dev/null +++ b/changelogs/unreleased/61629-dependency-installation-error-on-fsevents-1-2-4-with-node-js-12.yml @@ -0,0 +1,5 @@ +--- +title: Update indirect dependency fsevents from 1.2.4 to 1.2.9 +merge_request: 28220 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/61697-add-project-id-to-le-common-name.yml b/changelogs/unreleased/61697-add-project-id-to-le-common-name.yml new file mode 100644 index 00000000000..8ffa8d0a51a --- /dev/null +++ b/changelogs/unreleased/61697-add-project-id-to-le-common-name.yml @@ -0,0 +1,5 @@ +--- +title: Prevent common name collisions when requesting multiple Let's Encrypt certificates concurrently +merge_request: 28373 +author: +type: fixed diff --git a/changelogs/unreleased/61795-fix-error-when-moving-issues.yml b/changelogs/unreleased/61795-fix-error-when-moving-issues.yml new file mode 100644 index 00000000000..6812baa07c3 --- /dev/null +++ b/changelogs/unreleased/61795-fix-error-when-moving-issues.yml @@ -0,0 +1,5 @@ +--- +title: Remove unintended error message shown when moving issues +merge_request: 28317 +author: +type: fixed diff --git a/changelogs/unreleased/61914-fix-emojis-urls.yml b/changelogs/unreleased/61914-fix-emojis-urls.yml new file mode 100644 index 00000000000..578edf4a063 --- /dev/null +++ b/changelogs/unreleased/61914-fix-emojis-urls.yml @@ -0,0 +1,5 @@ +--- +title: Fix emojis URLs +merge_request: 28371 +author: +type: fixed diff --git a/changelogs/unreleased/add-branch-to-project-search-api.yml b/changelogs/unreleased/add-branch-to-project-search-api.yml new file mode 100644 index 00000000000..74cff94ab76 --- /dev/null +++ b/changelogs/unreleased/add-branch-to-project-search-api.yml @@ -0,0 +1,5 @@ +--- +title: Added ref querystring parameter to project search API to allow searching on branches/tags other than the default +merge_request: 28069 +author: Lee Tickett +type: added diff --git a/changelogs/unreleased/add-warning-to-backup-rake-task.yml b/changelogs/unreleased/add-warning-to-backup-rake-task.yml new file mode 100644 index 00000000000..7ddeae3f9fd --- /dev/null +++ b/changelogs/unreleased/add-warning-to-backup-rake-task.yml @@ -0,0 +1,5 @@ +--- +title: Add warning that gitlab-secrets isn't included in backup +merge_request: +author: +type: other diff --git a/changelogs/unreleased/allow-replying-to-individual-notes-from-api.yml b/changelogs/unreleased/allow-replying-to-individual-notes-from-api.yml new file mode 100644 index 00000000000..b268b0689ad --- /dev/null +++ b/changelogs/unreleased/allow-replying-to-individual-notes-from-api.yml @@ -0,0 +1,5 @@ +--- +title: Allow replying to individual notes from API +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/antonyliu-i18n-user-profile.yml b/changelogs/unreleased/antonyliu-i18n-user-profile.yml new file mode 100644 index 00000000000..f9065ee5697 --- /dev/null +++ b/changelogs/unreleased/antonyliu-i18n-user-profile.yml @@ -0,0 +1,5 @@ +--- +title: 'i18n: externalize strings from user profile settings' +merge_request: 28088 +author: Antony Liu +type: other diff --git a/changelogs/unreleased/ce-11430-update_clair_local_scan.yml b/changelogs/unreleased/ce-11430-update_clair_local_scan.yml new file mode 100644 index 00000000000..04bb04c3919 --- /dev/null +++ b/changelogs/unreleased/ce-11430-update_clair_local_scan.yml @@ -0,0 +1,5 @@ +--- +title: Update clair-local-scan to v2.0.8 for container scanning +merge_request: 27977 +author: +type: other diff --git a/changelogs/unreleased/ce-11542-remove-non-semantic-use-of-row-in-member-listing-controls.yml b/changelogs/unreleased/ce-11542-remove-non-semantic-use-of-row-in-member-listing-controls.yml new file mode 100644 index 00000000000..c2dcd309abd --- /dev/null +++ b/changelogs/unreleased/ce-11542-remove-non-semantic-use-of-row-in-member-listing-controls.yml @@ -0,0 +1,5 @@ +--- +title: Remove non-semantic use of `.row` in member listing controls +merge_request: 28204 +author: +type: fixed diff --git a/changelogs/unreleased/ce-57402-add-issues-statistics-api-endpoints.yml b/changelogs/unreleased/ce-57402-add-issues-statistics-api-endpoints.yml new file mode 100644 index 00000000000..a626193dc27 --- /dev/null +++ b/changelogs/unreleased/ce-57402-add-issues-statistics-api-endpoints.yml @@ -0,0 +1,5 @@ +--- +title: Add issues_statistics api endpoints and extend issues search api +merge_request: 27366 +author: +type: added diff --git a/changelogs/unreleased/ce-quick-fix-58727-collapsed-sidebar-flyout-menu-items-don-t-appear-in-1200px-screen-size.yml b/changelogs/unreleased/ce-quick-fix-58727-collapsed-sidebar-flyout-menu-items-don-t-appear-in-1200px-screen-size.yml new file mode 100644 index 00000000000..332105bb269 --- /dev/null +++ b/changelogs/unreleased/ce-quick-fix-58727-collapsed-sidebar-flyout-menu-items-don-t-appear-in-1200px-screen-size.yml @@ -0,0 +1,5 @@ +--- +title: Fix flyout nav on small viewports +merge_request: 25998 +author: +type: fixed diff --git a/changelogs/unreleased/diff-whitespace-setting-changes.yml b/changelogs/unreleased/diff-whitespace-setting-changes.yml new file mode 100644 index 00000000000..640e9e589df --- /dev/null +++ b/changelogs/unreleased/diff-whitespace-setting-changes.yml @@ -0,0 +1,5 @@ +--- +title: Fixed show whitespace button not refetching diff content +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/display-junit-classname-in-modal.yml b/changelogs/unreleased/display-junit-classname-in-modal.yml new file mode 100644 index 00000000000..c5140456e4e --- /dev/null +++ b/changelogs/unreleased/display-junit-classname-in-modal.yml @@ -0,0 +1,5 @@ +--- +title: Display classname JUnit attribute in report modal +merge_request: 28376 +author: +type: added diff --git a/changelogs/unreleased/dz-patch-58.yml b/changelogs/unreleased/dz-patch-58.yml new file mode 100644 index 00000000000..97ceadd303d --- /dev/null +++ b/changelogs/unreleased/dz-patch-58.yml @@ -0,0 +1,5 @@ +--- +title: Replace Oxygen-Sans font with Noto Sans +merge_request: 28322 +author: +type: changed diff --git a/changelogs/unreleased/fix-db-migrate-is-failed-on-mysql8.yml b/changelogs/unreleased/fix-db-migrate-is-failed-on-mysql8.yml new file mode 100644 index 00000000000..63f134808e3 --- /dev/null +++ b/changelogs/unreleased/fix-db-migrate-is-failed-on-mysql8.yml @@ -0,0 +1,5 @@ +--- +title: Fix. `db:migrate` is failed on MySQL 8 +merge_request: 28351 +author: sue445 +type: fixed diff --git a/changelogs/unreleased/fix-js-error-ssh-key-view.yml b/changelogs/unreleased/fix-js-error-ssh-key-view.yml new file mode 100644 index 00000000000..0615f2ee217 --- /dev/null +++ b/changelogs/unreleased/fix-js-error-ssh-key-view.yml @@ -0,0 +1,5 @@ +--- +title: disable SSH key validation in key details view +merge_request: 28180 +author: Roger Meier +type: fixed diff --git a/changelogs/unreleased/fix-project-visibility-level-validation.yml b/changelogs/unreleased/fix-project-visibility-level-validation.yml new file mode 100644 index 00000000000..9581a475842 --- /dev/null +++ b/changelogs/unreleased/fix-project-visibility-level-validation.yml @@ -0,0 +1,5 @@ +--- +title: Fix project visibility level validation +merge_request: 28305 +author: Peter Marko +type: fixed diff --git a/changelogs/unreleased/fix-schedule-head-pipeline-update-method.yml b/changelogs/unreleased/fix-schedule-head-pipeline-update-method.yml new file mode 100644 index 00000000000..5e574ef686c --- /dev/null +++ b/changelogs/unreleased/fix-schedule-head-pipeline-update-method.yml @@ -0,0 +1,5 @@ +--- +title: Fix update head pipeline process of Pipelines for merge requests +merge_request: 28057 +author: +type: fixed diff --git a/changelogs/unreleased/fix-too-many-loops-cron-error.yml b/changelogs/unreleased/fix-too-many-loops-cron-error.yml new file mode 100644 index 00000000000..a9b5b761439 --- /dev/null +++ b/changelogs/unreleased/fix-too-many-loops-cron-error.yml @@ -0,0 +1,5 @@ +--- +title: Fix "too many loops" error by handling gracefully cron schedules for non existent days +merge_request: 28002 +author: +type: fixed diff --git a/changelogs/unreleased/fj-59522-improve-search-controller-performance.yml b/changelogs/unreleased/fj-59522-improve-search-controller-performance.yml new file mode 100644 index 00000000000..c513f3c3aeb --- /dev/null +++ b/changelogs/unreleased/fj-59522-improve-search-controller-performance.yml @@ -0,0 +1,5 @@ +--- +title: Add improvements to global search of issues and merge requests +merge_request: 27817 +author: +type: performance diff --git a/changelogs/unreleased/fl-fix-next-flag-for-good.yml b/changelogs/unreleased/fl-fix-next-flag-for-good.yml new file mode 100644 index 00000000000..93f27824213 --- /dev/null +++ b/changelogs/unreleased/fl-fix-next-flag-for-good.yml @@ -0,0 +1,5 @@ +--- +title: Next badge must visible when canary flag is true +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/friendly-wrap-component.yml b/changelogs/unreleased/friendly-wrap-component.yml new file mode 100644 index 00000000000..c16ca0af287 --- /dev/null +++ b/changelogs/unreleased/friendly-wrap-component.yml @@ -0,0 +1,5 @@ +--- +title: Add CSS fix for <wbr> elements on IE11 +merge_request: 27846 +author: +type: other diff --git a/changelogs/unreleased/gitaly-version-v1.42.0.yml b/changelogs/unreleased/gitaly-version-v1.42.0.yml new file mode 100644 index 00000000000..38621fa071e --- /dev/null +++ b/changelogs/unreleased/gitaly-version-v1.42.0.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade to Gitaly v1.42.0 +merge_request: 28135 +author: +type: changed diff --git a/changelogs/unreleased/include-ee-fixtures.yml b/changelogs/unreleased/include-ee-fixtures.yml new file mode 100644 index 00000000000..ba500d92de3 --- /dev/null +++ b/changelogs/unreleased/include-ee-fixtures.yml @@ -0,0 +1,5 @@ +--- +title: Add EE fixtures to SeedFu list +merge_request: 28241 +author: +type: other diff --git a/changelogs/unreleased/instance_level_clusters.yml b/changelogs/unreleased/instance_level_clusters.yml new file mode 100644 index 00000000000..afd06a4e05f --- /dev/null +++ b/changelogs/unreleased/instance_level_clusters.yml @@ -0,0 +1,5 @@ +--- +title: Instance level kubernetes clusters +merge_request: 27196 +author: +type: added diff --git a/changelogs/unreleased/jc-omit-count-diverging-commits-max.yml b/changelogs/unreleased/jc-omit-count-diverging-commits-max.yml new file mode 100644 index 00000000000..23235060a98 --- /dev/null +++ b/changelogs/unreleased/jc-omit-count-diverging-commits-max.yml @@ -0,0 +1,5 @@ +--- +title: Omit max-count for diverging_commit_counts behind feature flag +merge_request: 28157 +author: +type: other diff --git a/changelogs/unreleased/kinolaev-master-patch-13154.yml b/changelogs/unreleased/kinolaev-master-patch-13154.yml new file mode 100644 index 00000000000..3292ff797e2 --- /dev/null +++ b/changelogs/unreleased/kinolaev-master-patch-13154.yml @@ -0,0 +1,5 @@ +--- +title: 'Auto-DevOps: allow to disable rollout status check' +merge_request: 28130 +author: Sergej Nikolaev <kinolaev@gmail.com> +type: fixed diff --git a/changelogs/unreleased/leipert-node-12-compatibility.yml b/changelogs/unreleased/leipert-node-12-compatibility.yml new file mode 100644 index 00000000000..18025d33a6d --- /dev/null +++ b/changelogs/unreleased/leipert-node-12-compatibility.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade dependencies for node 12 compatibility +merge_request: 28323 +author: +type: fixed diff --git a/changelogs/unreleased/make-autocomplete-faster-with-lots-of-results.yml b/changelogs/unreleased/make-autocomplete-faster-with-lots-of-results.yml new file mode 100644 index 00000000000..daeefd3ffd7 --- /dev/null +++ b/changelogs/unreleased/make-autocomplete-faster-with-lots-of-results.yml @@ -0,0 +1,5 @@ +--- +title: Improve performance of users autocomplete when there are lots of results +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/mm12935.yml b/changelogs/unreleased/mm12935.yml new file mode 100644 index 00000000000..782586c514e --- /dev/null +++ b/changelogs/unreleased/mm12935.yml @@ -0,0 +1,5 @@ +--- +title: Remove Content-Type override for Mattermost OAuth login +merge_request: +author: Harrison Healey +type: removed diff --git a/changelogs/unreleased/sh-cleanup-import-export.yml b/changelogs/unreleased/sh-cleanup-import-export.yml new file mode 100644 index 00000000000..3d5d6f3c907 --- /dev/null +++ b/changelogs/unreleased/sh-cleanup-import-export.yml @@ -0,0 +1,5 @@ +--- +title: Clean up CarrierWave's import/export files +merge_request: 27487 +author: +type: fixed diff --git a/changelogs/unreleased/sh-fix-invited-members.yml b/changelogs/unreleased/sh-fix-invited-members.yml new file mode 100644 index 00000000000..96e43e1aa53 --- /dev/null +++ b/changelogs/unreleased/sh-fix-invited-members.yml @@ -0,0 +1,5 @@ +--- +title: Fix Error 500 when inviting user already present +merge_request: 28198 +author: +type: fixed diff --git a/changelogs/unreleased/sh-fix-lfs-download-errors.yml b/changelogs/unreleased/sh-fix-lfs-download-errors.yml new file mode 100644 index 00000000000..ad67df6bb06 --- /dev/null +++ b/changelogs/unreleased/sh-fix-lfs-download-errors.yml @@ -0,0 +1,5 @@ +--- +title: Properly handle LFS Batch API response in project import +merge_request: 28223 +author: +type: fixed diff --git a/changelogs/unreleased/sh-fix-personal-snippet-uploads-object-storage.yml b/changelogs/unreleased/sh-fix-personal-snippet-uploads-object-storage.yml new file mode 100644 index 00000000000..603afa8573f --- /dev/null +++ b/changelogs/unreleased/sh-fix-personal-snippet-uploads-object-storage.yml @@ -0,0 +1,5 @@ +--- +title: Fix incorrect prefix used in new uploads for personal snippets +merge_request: 28337 +author: +type: fixed diff --git a/changelogs/unreleased/sh-fix-rebase-error-clearing.yml b/changelogs/unreleased/sh-fix-rebase-error-clearing.yml new file mode 100644 index 00000000000..4f5f2779e7f --- /dev/null +++ b/changelogs/unreleased/sh-fix-rebase-error-clearing.yml @@ -0,0 +1,5 @@ +--- +title: Properly clear the merge error upon rebase failure +merge_request: 28319 +author: +type: fixed diff --git a/changelogs/unreleased/sh-fix-related-merge-requests-path.yml b/changelogs/unreleased/sh-fix-related-merge-requests-path.yml new file mode 100644 index 00000000000..4b4108feda4 --- /dev/null +++ b/changelogs/unreleased/sh-fix-related-merge-requests-path.yml @@ -0,0 +1,5 @@ +--- +title: Use a path for the related merge requests endpoint +merge_request: 28171 +author: +type: fixed diff --git a/changelogs/unreleased/sh-fix-rugged-get-tree-entries-recursive.yml b/changelogs/unreleased/sh-fix-rugged-get-tree-entries-recursive.yml new file mode 100644 index 00000000000..a9d46c6f460 --- /dev/null +++ b/changelogs/unreleased/sh-fix-rugged-get-tree-entries-recursive.yml @@ -0,0 +1,5 @@ +--- +title: Fix Rugged get_tree_entries recursive flag not working +merge_request: 28494 +author: +type: fixed diff --git a/changelogs/unreleased/sh-fix-tag-push-remote-mirror.yml b/changelogs/unreleased/sh-fix-tag-push-remote-mirror.yml new file mode 100644 index 00000000000..7f33ab28e3d --- /dev/null +++ b/changelogs/unreleased/sh-fix-tag-push-remote-mirror.yml @@ -0,0 +1,5 @@ +--- +title: Fix remote mirrors not updating after tag push +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/sh-update-process-mem.yml b/changelogs/unreleased/sh-update-process-mem.yml new file mode 100644 index 00000000000..51b22fb0f00 --- /dev/null +++ b/changelogs/unreleased/sh-update-process-mem.yml @@ -0,0 +1,5 @@ +--- +title: Update get_process_mem to 0.2.3 +merge_request: 28248 +author: +type: other diff --git a/changelogs/unreleased/sh-upgrade-ruby-2-6-3-ce.yml b/changelogs/unreleased/sh-upgrade-ruby-2-6-3-ce.yml new file mode 100644 index 00000000000..9ad5c9ebb64 --- /dev/null +++ b/changelogs/unreleased/sh-upgrade-ruby-2-6-3-ce.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade Ruby version to 2.6.3 +merge_request: 28117 +author: +type: performance diff --git a/changelogs/unreleased/shell-9-1-0.yml b/changelogs/unreleased/shell-9-1-0.yml new file mode 100644 index 00000000000..d5a01ee57ee --- /dev/null +++ b/changelogs/unreleased/shell-9-1-0.yml @@ -0,0 +1,5 @@ +--- +title: Update gitlab-shell to v9.1.0 +merge_request: 28184 +author: +type: other diff --git a/changelogs/unreleased/show-disabled-mirrors.yml b/changelogs/unreleased/show-disabled-mirrors.yml new file mode 100644 index 00000000000..a401606b331 --- /dev/null +++ b/changelogs/unreleased/show-disabled-mirrors.yml @@ -0,0 +1,5 @@ +--- +title: Show disabled project repo mirrors in settings +merge_request: 27326 +author: +type: other diff --git a/changelogs/unreleased/update-babel-to-7-4-4.yml b/changelogs/unreleased/update-babel-to-7-4-4.yml new file mode 100644 index 00000000000..0d4b4700bb2 --- /dev/null +++ b/changelogs/unreleased/update-babel-to-7-4-4.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade babel to 7.4.4 +merge_request: 28437 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/update-gitaly-to-v1-42-1.yml b/changelogs/unreleased/update-gitaly-to-v1-42-1.yml new file mode 100644 index 00000000000..ff42bdd9c0b --- /dev/null +++ b/changelogs/unreleased/update-gitaly-to-v1-42-1.yml @@ -0,0 +1,5 @@ +--- +title: "Update Gitaly to v1.42.1" +merge_request: 28425 +author: +type: other diff --git a/changelogs/unreleased/winh-notes-error-handling.yml b/changelogs/unreleased/winh-notes-error-handling.yml new file mode 100644 index 00000000000..6f23dd459d4 --- /dev/null +++ b/changelogs/unreleased/winh-notes-error-handling.yml @@ -0,0 +1,5 @@ +--- +title: Handle errors in successful notes reply +merge_request: 28082 +author: +type: fixed diff --git a/changelogs/unreleased/zj-usage-ping-pool-repository.yml b/changelogs/unreleased/zj-usage-ping-pool-repository.yml new file mode 100644 index 00000000000..62044a933d0 --- /dev/null +++ b/changelogs/unreleased/zj-usage-ping-pool-repository.yml @@ -0,0 +1,5 @@ +--- +title: Add Pool repository to the usage ping +merge_request: 28267 +author: +type: other diff --git a/config/environments/test.rb b/config/environments/test.rb index 3461099253a..e7166882eea 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -11,7 +11,7 @@ Rails.application.configure do # and recreated between test runs. Don't rely on the data there! # Enabling caching of classes slows start-up time because all controllers - # are loaded at initalization, but it reduces memory and load because files + # are loaded at initialization, but it reduces memory and load because files # are not reloaded with every request. For example, caching is not necessary # for loading database migrations but useful for handling Knapsack specs. config.cache_classes = ENV['CACHE_CLASSES'] == 'true' diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 2f822805b25..23377b43f78 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -245,7 +245,7 @@ production: &base host: example.com port: 80 # Set to 443 if you serve the pages with HTTPS https: false # Set to true if you serve the pages with HTTPS - artifacts_server: true + artifacts_server: true # Set to false if you want to disable online view of HTML artifacts # external_http: ["1.1.1.1:80", "[2001::1]:80"] # If defined, enables custom domain support in GitLab Pages # external_https: ["1.1.1.1:443", "[2001::1]:443"] # If defined, enables custom domain and certificate support in GitLab Pages admin: @@ -940,6 +940,10 @@ test: app_id: 'YOUR_CLIENT_ID', app_secret: 'YOUR_CLIENT_SECRET', args: { scope: 'aq:name email~rs address aq:push' } } + - { name: 'salesforce', + app_id: 'YOUR_CLIENT_ID', + app_secret: 'YOUR_CLIENT_SECRET' + } ldap: enabled: false servers: diff --git a/config/initializers/01_secret_token.rb b/config/initializers/01_secret_token.rb index 02bded43083..e24b5cbd510 100644 --- a/config/initializers/01_secret_token.rb +++ b/config/initializers/01_secret_token.rb @@ -1,3 +1,14 @@ +# WARNING: If you add a new secret to this file, make sure you also +# update Omnibus GitLab or updates will fail. Omnibus is responsible for +# writing the `secrets.yml` file. If Omnibus doesn't know about a +# secret, Rails will attempt to write to the file, but this will fail +# because Rails doesn't have write access. +# +# As an example: +# * https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/27581 +# * https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/3267 +# +# # This file needs to be loaded BEFORE any initializers that attempt to # prepend modules that require access to secrets (e.g. EE's 0_as_concern.rb). # @@ -28,7 +39,8 @@ def create_tokens 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, - openid_connect_signing_key: generate_new_rsa_private_key + openid_connect_signing_key: generate_new_rsa_private_key, + lets_encrypt_private_key: generate_lets_encrypt_private_key } missing_secrets = set_missing_keys(defaults) @@ -49,6 +61,10 @@ def generate_new_rsa_private_key OpenSSL::PKey::RSA.new(2048).to_pem end +def generate_lets_encrypt_private_key + OpenSSL::PKey::RSA.new(4096).to_pem +end + 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 diff --git a/config/initializers/action_dispatch_http_mime_negotiation.rb b/config/initializers/action_dispatch_http_mime_negotiation.rb index bdf5b0babfb..6c31de2de55 100644 --- a/config/initializers/action_dispatch_http_mime_negotiation.rb +++ b/config/initializers/action_dispatch_http_mime_negotiation.rb @@ -2,7 +2,7 @@ # the extension of the full URL path if no explicit `format` param or `Accept` # header is provided, like when simply browsing to a page in your browser. # -# This is undesireable in GitLab, because many of our paths will end in a ref or +# This is undesirable in GitLab, because many of our paths will end in a ref or # blob name that can end with any extension, while these pages should still be # presented as HTML unless otherwise specified. diff --git a/config/initializers/config_initializers_active_record_locking.rb b/config/initializers/config_initializers_active_record_locking.rb index 1c4352b135d..608d63223a3 100644 --- a/config/initializers/config_initializers_active_record_locking.rb +++ b/config/initializers/config_initializers_active_record_locking.rb @@ -1,4 +1,8 @@ # frozen_string_literal: true + +# ensure ActiveRecord's version has been required already +require 'active_record/locking/optimistic' + # rubocop:disable Lint/RescueException module ActiveRecord module Locking @@ -16,7 +20,7 @@ module ActiveRecord self[locking_column] += 1 # Patched because when `lock_version` is read as `0`, it may actually be `NULL` in the DB. - possible_previous_lock_value = previous_lock_value == 0 ? [nil, 0] : previous_lock_value + possible_previous_lock_value = previous_lock_value.to_i == 0 ? [nil, 0] : previous_lock_value affected_rows = self.class.unscoped._update_record( arel_attributes_with_values(attribute_names), diff --git a/config/initializers/seed_fu.rb b/config/initializers/seed_fu.rb new file mode 100644 index 00000000000..2e48e41a311 --- /dev/null +++ b/config/initializers/seed_fu.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true +if Gitlab.ee? + SeedFu.fixture_paths += %W[ee/db/fixtures ee/db/fixtures/#{Rails.env}] +end diff --git a/config/karma.config.js b/config/karma.config.js index dfcb5c4646e..b2fc3a32816 100644 --- a/config/karma.config.js +++ b/config/karma.config.js @@ -4,6 +4,7 @@ const chalk = require('chalk'); const webpack = require('webpack'); const argumentsParser = require('commander'); const webpackConfig = require('./webpack.config.js'); +const IS_EE = require('./helpers/is_ee_env'); const ROOT_PATH = path.resolve(__dirname, '..'); const SPECS_PATH = /^(?:\.[\\\/])?(ee[\\\/])?spec[\\\/]javascripts[\\\/]/; @@ -90,6 +91,8 @@ if (specFilters.length) { module.exports = function(config) { process.env.TZ = 'Etc/UTC'; + const fixturesPath = `${IS_EE ? 'ee/' : ''}spec/javascripts/fixtures`; + const karmaConfig = { basePath: ROOT_PATH, browsers: ['ChromeHeadlessCustom'], @@ -104,13 +107,15 @@ module.exports = function(config) { // chrome cannot run in sandboxed mode inside a docker container unless it is run with // escalated kernel privileges (e.g. docker run --cap-add=CAP_SYS_ADMIN) '--no-sandbox', + // https://bugs.chromium.org/p/chromedriver/issues/detail?id=2870 + '--enable-features=NetworkService,NetworkServiceInProcess', ], }, }, frameworks: ['jasmine'], files: [ { pattern: 'spec/javascripts/test_bundle.js', watched: false }, - { pattern: `spec/javascripts/fixtures/**/*@(.json|.html|.png|.bmpr|.pdf)`, included: false }, + { pattern: `${fixturesPath}/**/*@(.json|.html|.png|.bmpr|.pdf)`, included: false }, ], preprocessors: { 'spec/javascripts/**/*.js': ['webpack', 'sourcemap'], diff --git a/config/puma.rb.example b/config/puma.rb.example new file mode 100644 index 00000000000..6558dbc6cfe --- /dev/null +++ b/config/puma.rb.example @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +# Load "path" as a rackup file. +# +# The default is "config.ru". +# +rackup 'config.ru' +pidfile '/home/git/gitlab/tmp/pids/puma.pid' +state_path '/home/git/gitlab/tmp/pids/puma.state' + +stdout_redirect '/home/git/gitlab/log/puma.stdout.log', + '/home/git/gitlab/log/puma.stderr.log', + true + +# Configure "min" to be the minimum number of threads to use to answer +# requests and "max" the maximum. +# +# The default is "0, 16". +# +threads 1, 16 + +# By default, workers accept all requests and queue them to pass to handlers. +# When false, workers accept the number of simultaneous requests configured. +# +# Queueing requests generally improves performance, but can cause deadlocks if +# the app is waiting on a request to itself. See https://github.com/puma/puma/issues/612 +# +# When set to false this may require a reverse proxy to handle slow clients and +# queue requests before they reach puma. This is due to disabling HTTP keepalive +queue_requests false + +# Bind the server to "url". "tcp://", "unix://" and "ssl://" are the only +# accepted protocols. +bind 'unix:///home/git/gitlab/tmp/sockets/gitlab.socket' + +workers 3 + +require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events" +require_relative "/home/git/gitlab/lib/gitlab/cluster/puma_worker_killer_initializer" + +on_restart do + # Signal application hooks that we're about to restart + Gitlab::Cluster::LifecycleEvents.do_master_restart +end + +before_fork do + # Signal to the puma killer + Gitlab::Cluster::PumaWorkerKillerInitializer.start @config.options unless ENV['DISABLE_PUMA_WORKER_KILLER'] + + # Signal application hooks that we're about to fork + Gitlab::Cluster::LifecycleEvents.do_before_fork +end + +Gitlab::Cluster::LifecycleEvents.set_puma_options @config.options +on_worker_boot do + # Signal application hooks of worker start + Gitlab::Cluster::LifecycleEvents.do_worker_start +end + +# Preload the application before starting the workers; this conflicts with +# phased restart feature. (off by default) +preload_app! + +tag 'gitlab-puma-worker' + +# Verifies that all workers have checked in to the master process within +# the given timeout. If not the worker process will be restarted. Default +# value is 60 seconds. +# +worker_timeout 60 diff --git a/config/routes/admin.rb b/config/routes/admin.rb index a01003b6039..bc19219a0b8 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -111,6 +111,7 @@ namespace :admin do put :reset_health_check_token put :clear_repository_check_states get :integrations, :repository, :templates, :ci_cd, :reporting, :metrics_and_profiling, :network, :geo, :preferences + get :lets_encrypt_terms_of_service end resources :labels @@ -132,5 +133,7 @@ namespace :admin do end end + concerns :clusterable + root to: 'dashboard#index' end diff --git a/config/unicorn.rb.example.development b/config/unicorn.rb.example.development index f7541bb9d55..ae3dc2e37e1 100644 --- a/config/unicorn.rb.example.development +++ b/config/unicorn.rb.example.development @@ -58,4 +58,3 @@ after_fork do |server, worker| # addr = "127.0.0.1:#{9293 + worker.nr}" # server.listen(addr, :tries => -1, :delay => 5, :tcp_nopush => true) end - diff --git a/db/migrate/20150509180749_convert_legacy_reference_notes.rb b/db/migrate/20150509180749_convert_legacy_reference_notes.rb index a44a908c2f5..84d4eb9e51f 100644 --- a/db/migrate/20150509180749_convert_legacy_reference_notes.rb +++ b/db/migrate/20150509180749_convert_legacy_reference_notes.rb @@ -7,7 +7,8 @@ # mentioned in 54f7727c850972f0401c1312a7c4a6a380de5666 class ConvertLegacyReferenceNotes < ActiveRecord::Migration[4.2] def up - execute %q{UPDATE notes SET note = trim(both '_' from note) WHERE system = true AND note LIKE '\_%\_'} + quoted_column_name = ActiveRecord::Base.connection.quote_column_name('system') + execute %Q{UPDATE notes SET note = trim(both '_' from note) WHERE #{quoted_column_name} = true AND note LIKE '\_%\_'} end def down diff --git a/db/migrate/20170330141723_disable_invalid_service_templates2.rb b/db/migrate/20170330141723_disable_invalid_service_templates2.rb index 91ec19dfa87..f09f3b3e355 100644 --- a/db/migrate/20170330141723_disable_invalid_service_templates2.rb +++ b/db/migrate/20170330141723_disable_invalid_service_templates2.rb @@ -1,5 +1,5 @@ # This is the same as DisableInvalidServiceTemplates. Later migrations may have -# inadventently enabled some invalid templates again. +# inadvertently enabled some invalid templates again. # class DisableInvalidServiceTemplates2 < ActiveRecord::Migration[4.2] DOWNTIME = false diff --git a/db/migrate/20190325105715_add_fields_to_user_preferences.rb b/db/migrate/20190325105715_add_fields_to_user_preferences.rb new file mode 100644 index 00000000000..9ea3b4f9cd8 --- /dev/null +++ b/db/migrate/20190325105715_add_fields_to_user_preferences.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddFieldsToUserPreferences < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + DOWNTIME = false + + def up + add_column(:user_preferences, :timezone, :string) + add_column(:user_preferences, :time_display_relative, :boolean) + add_column(:user_preferences, :time_format_in_24h, :boolean) + end + + def down + remove_column(:user_preferences, :timezone) + remove_column(:user_preferences, :time_display_relative) + remove_column(:user_preferences, :time_format_in_24h) + end +end diff --git a/db/migrate/20190412155659_add_merge_request_blocks.rb b/db/migrate/20190412155659_add_merge_request_blocks.rb new file mode 100644 index 00000000000..9e7f370d1cf --- /dev/null +++ b/db/migrate/20190412155659_add_merge_request_blocks.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class AddMergeRequestBlocks < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + create_table :merge_request_blocks, id: :bigserial do |t| + t.references :blocking_merge_request, + index: false, null: false, + foreign_key: { to_table: :merge_requests, on_delete: :cascade } + + t.references :blocked_merge_request, + index: true, null: false, + foreign_key: { to_table: :merge_requests, on_delete: :cascade } + + t.index [:blocking_merge_request_id, :blocked_merge_request_id], + unique: true, + name: 'index_mr_blocks_on_blocking_and_blocked_mr_ids' + + t.timestamps_with_timezone + end + end +end diff --git a/db/migrate/20190515125613_add_application_settings_elasticsearch_shards.rb b/db/migrate/20190515125613_add_application_settings_elasticsearch_shards.rb new file mode 100644 index 00000000000..9cebc0f8db4 --- /dev/null +++ b/db/migrate/20190515125613_add_application_settings_elasticsearch_shards.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class AddApplicationSettingsElasticsearchShards < ActiveRecord::Migration[5.1] + DOWNTIME = false + + def change + add_column :application_settings, :elasticsearch_shards, :integer, null: false, default: 5 + add_column :application_settings, :elasticsearch_replicas, :integer, null: false, default: 1 + end +end diff --git a/db/migrate/20190516011213_add_build_queued_at_index.rb b/db/migrate/20190516011213_add_build_queued_at_index.rb new file mode 100644 index 00000000000..77ffa7cd4e9 --- /dev/null +++ b/db/migrate/20190516011213_add_build_queued_at_index.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# This migration make queued_at field indexed to speed up builds filtering by job_age + +class AddBuildQueuedAtIndex < ActiveRecord::Migration[5.1] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :ci_builds, :queued_at + end + + def down + remove_concurrent_index :ci_builds, :queued_at + end +end diff --git a/db/post_migrate/20190301081611_migrate_project_migrate_sidekiq_queue.rb b/db/post_migrate/20190301081611_migrate_project_migrate_sidekiq_queue.rb index 6af7902e0c4..46108d142b5 100644 --- a/db/post_migrate/20190301081611_migrate_project_migrate_sidekiq_queue.rb +++ b/db/post_migrate/20190301081611_migrate_project_migrate_sidekiq_queue.rb @@ -5,8 +5,6 @@ class MigrateProjectMigrateSidekiqQueue < ActiveRecord::Migration[5.0] DOWNTIME = false - DOWNTIME = false - def up sidekiq_queue_migrate 'project_migrate_hashed_storage', to: 'hashed_storage:hashed_storage_project_migrate' end diff --git a/db/post_migrate/20190424134256_drop_projects_ci_id.rb b/db/post_migrate/20190424134256_drop_projects_ci_id.rb index 79fa9704f1f..44e8f316393 100644 --- a/db/post_migrate/20190424134256_drop_projects_ci_id.rb +++ b/db/post_migrate/20190424134256_drop_projects_ci_id.rb @@ -22,7 +22,12 @@ class DropProjectsCiId < ActiveRecord::Migration[5.1] end def down - add_column :projects, :ci_id, :integer - add_concurrent_index :projects, :ci_id + unless column_exists?(:projects, :ci_id) + add_column :projects, :ci_id, :integer + end + + unless index_exists?(:projects, :ci_id) + add_concurrent_index :projects, :ci_id + end end end diff --git a/db/schema.rb b/db/schema.rb index deaf406fe3d..412b5313b69 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20190506135400) do +ActiveRecord::Schema.define(version: 20190516011213) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -189,6 +189,8 @@ ActiveRecord::Schema.define(version: 20190506135400) do t.string "encrypted_external_auth_client_key_pass_iv" t.string "lets_encrypt_notification_email" t.boolean "lets_encrypt_terms_of_service_accepted", default: false, null: false + t.integer "elasticsearch_shards", default: 5, null: false + t.integer "elasticsearch_replicas", default: 1, null: false t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree end @@ -378,6 +380,7 @@ ActiveRecord::Schema.define(version: 20190506135400) do t.index ["project_id", "id"], name: "index_ci_builds_on_project_id_and_id", using: :btree t.index ["project_id", "status"], name: "index_ci_builds_project_id_and_status_for_live_jobs_partial2", where: "(((type)::text = 'Ci::Build'::text) AND ((status)::text = ANY (ARRAY[('running'::character varying)::text, ('pending'::character varying)::text, ('created'::character varying)::text])))", using: :btree t.index ["protected"], name: "index_ci_builds_on_protected", using: :btree + t.index ["queued_at"], name: "index_ci_builds_on_queued_at", using: :btree t.index ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree t.index ["scheduled_at"], name: "partial_index_ci_builds_on_scheduled_at_with_scheduled_jobs", where: "((scheduled_at IS NOT NULL) AND ((type)::text = 'Ci::Build'::text) AND ((status)::text = 'scheduled'::text))", using: :btree t.index ["stage_id", "stage_idx"], name: "tmp_build_stage_position_index", where: "(stage_idx IS NOT NULL)", using: :btree @@ -1227,6 +1230,15 @@ ActiveRecord::Schema.define(version: 20190506135400) do t.index ["user_id"], name: "index_merge_request_assignees_on_user_id", using: :btree end + create_table "merge_request_blocks", force: :cascade do |t| + t.integer "blocking_merge_request_id", null: false + t.integer "blocked_merge_request_id", null: false + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false + t.index ["blocked_merge_request_id"], name: "index_merge_request_blocks_on_blocked_merge_request_id", using: :btree + t.index ["blocking_merge_request_id", "blocked_merge_request_id"], name: "index_mr_blocks_on_blocking_and_blocked_mr_ids", unique: true, using: :btree + end + create_table "merge_request_diff_commits", id: false, force: :cascade do |t| t.datetime_with_timezone "authored_date" t.datetime_with_timezone "committed_date" @@ -2246,6 +2258,9 @@ ActiveRecord::Schema.define(version: 20190506135400) do t.integer "first_day_of_week" t.string "issues_sort" t.string "merge_requests_sort" + t.string "timezone" + t.boolean "time_display_relative" + t.boolean "time_format_in_24h" t.index ["user_id"], name: "index_user_preferences_on_user_id", unique: true, using: :btree end @@ -2511,6 +2526,8 @@ ActiveRecord::Schema.define(version: 20190506135400) do add_foreign_key "members", "users", name: "fk_2e88fb7ce9", on_delete: :cascade add_foreign_key "merge_request_assignees", "merge_requests", on_delete: :cascade add_foreign_key "merge_request_assignees", "users", on_delete: :cascade + add_foreign_key "merge_request_blocks", "merge_requests", column: "blocked_merge_request_id", on_delete: :cascade + add_foreign_key "merge_request_blocks", "merge_requests", column: "blocking_merge_request_id", on_delete: :cascade add_foreign_key "merge_request_diff_commits", "merge_request_diffs", on_delete: :cascade add_foreign_key "merge_request_diff_files", "merge_request_diffs", on_delete: :cascade add_foreign_key "merge_request_diffs", "merge_requests", name: "fk_8483f3258f", on_delete: :cascade diff --git a/doc/README.md b/doc/README.md index dd4909ce303..3863e17c268 100644 --- a/doc/README.md +++ b/doc/README.md @@ -4,7 +4,7 @@ description: 'Learn how to use and administer GitLab, the most scalable Git-base --- <div class="display-none"> - <em>Visit <a href="https://docs.gitlab.com/ce/">docs.gitlab.com</a> for optimized + <em>Visit <a href="https://docs.gitlab.com/ee/">docs.gitlab.com</a> for optimized navigation, discoverability, and readability.</em> </div> <!-- the div above will not display on the docs site but will display on /help --> @@ -107,13 +107,18 @@ The following documentation relates to the DevOps **Plan** stage: | Plan Topics | Description | |:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------| +| [Burndown Charts](user/project/milestones/burndown_charts.md) **[STARTER]** | Watch your project's progress throughout a specific milestone. | | [Discussions](user/discussions/index.md) | Threads, comments, and resolvable discussions in issues, commits, and merge requests. | | [Due Dates](user/project/issues/due_dates.md) | Keep track of issue deadlines. | -| [Quick Actions](user/project/quick_actions.md) | Shortcuts for common actions on issues or merge requests, replacing the need to click buttons or use dropdowns in GitLab's UI. | +| [Epics](user/group/epics/index.md) **[ULTIMATE]** | Tracking groups of issues that share a theme. | | [Issues](user/project/issues/index.md), including [confidential issues](user/project/issues/confidential_issues.md),<br/>[issue and merge request templates](user/project/description_templates.md),<br/>and [moving issues](user/project/issues/moving_issues.md) | Project issues, restricting access to issues, create templates for submitting new issues and merge requests, and moving issues between projects. | | [Labels](user/project/labels.md) | Categorize issues or merge requests with descriptive labels. | | [Milestones](user/project/milestones/index.md) | Set milestones for delivery of issues and merge requests, with optional due date. | | [Project Issue Board](user/project/issue_board.md) | Display issues on a Scrum or Kanban board. | +| [Quick Actions](user/project/quick_actions.md) | Shortcuts for common actions on issues or merge requests, replacing the need to click buttons or use dropdowns in GitLab's UI. | +| [Related Issues](user/project/issues/related_issues.md) **[STARTER]** | Create a relationship between issues. | +| [Roadmap](user/group/roadmap/index.md) **[ULTIMATE]** | Visualize epic timelines. | +| [Service Desk](user/project/service_desk.md) **[PREMIUM]** | A simple way to allow people to create issues in your GitLab instance without needing their own user account. | | [Time Tracking](workflow/time_tracking.md) | Track time spent on issues and merge requests. | | [Todos](workflow/todos.md) | Keep track of work requiring attention with a chronological list displayed on a simple dashboard. | @@ -136,16 +141,21 @@ The following documentation relates to the DevOps **Create** stage: #### Projects and Groups -| Create Topics - Projects and Groups | Description | -|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------| -| [Create](gitlab-basics/create-project.md) and [fork](gitlab-basics/fork-project.md) projects, and<br/>[import and export<br/>projects between instances](user/project/settings/import_export.md) | Create, duplicate, and move projects. | -| [GitLab Pages](user/project/pages/index.md) | Build, test, and deploy your static website with GitLab Pages. | -| [Groups](user/group/index.md) and [Subgroups](user/group/subgroups/index.md) | Organize your projects in groups. | -| [Projects](user/project/index.md), including [project access](public_access/public_access.md)<br/>and [settings](user/project/settings/index.md) | Host source code, and control your project's visibility and set configuration. | -| [Search through GitLab](user/search/index.md) | Search for issues, merge requests, projects, groups, and todos. | -| [Snippets](user/snippets.md) | Snippets allow you to create little bits of code. | -| [Web IDE](user/project/web_ide/index.md) | Edit files within GitLab's user interface. | -| [Wikis](user/project/wiki/index.md) | Enhance your repository documentation with built-in wikis. | +| Create Topics - Projects and Groups | Description | +|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------| +| [Advanced global search](user/search/advanced_global_search.md) **[STARTER]** | Leverage Elasticsearch for faster, more advanced code search across your entire GitLab instance. | +| [Advanced syntax search](user/search/advanced_search_syntax.md) **[STARTER]** | Use advanced queries for more targeted search results. | +| [Contribution analytics](user/group/contribution_analytics/index.md) **[STARTER]** | See detailed statistics of group contributors. | +| [Create](gitlab-basics/create-project.md) and [fork](gitlab-basics/fork-project.md) projects, and<br/>[import and export projects<br/>between instances](user/project/settings/import_export.md) | Create, duplicate, and move projects. | +| [File locking](user/project/file_lock.md) **[PREMIUM]** | Lock files to avoid merge conflicts. | +| [GitLab Pages](user/project/pages/index.md) | Build, test, and deploy your static website with GitLab Pages. | +| [Groups](user/group/index.md) and [Subgroups](user/group/subgroups/index.md) | Organize your projects in groups. | +| [Issues Analytics](user/group/issues_analytics/index.md) **[PREMIUM]** | Check how many issues were created per month. | +| [Projects](user/project/index.md), including [project access](public_access/public_access.md)<br/>and [settings](user/project/settings/index.md) | Host source code, and control your project's visibility and set configuration. | +| [Search through GitLab](user/search/index.md) | Search for issues, merge requests, projects, groups, and todos. | +| [Snippets](user/snippets.md) | Snippets allow you to create little bits of code. | +| [Web IDE](user/project/web_ide/index.md) | Edit files within GitLab's user interface. | +| [Wikis](user/project/wiki/index.md) | Enhance your repository documentation with built-in wikis. | <div align="right"> <a type="button" class="btn btn-default" href="#overview"> @@ -165,7 +175,9 @@ The following documentation relates to the DevOps **Create** stage: | [Files](user/project/repository/index.md#files) | Files management. | | [Jupyter Notebook files](user/project/repository/index.md#jupyter-notebook-files) | GitLab's support for `.ipynb` files. | | [Protected branches](user/project/protected_branches.md) | Use protected branches. | +| [Push rules](push_rules/push_rules.md) **[STARTER]** | Additional control over pushes to your projects. | | [Repositories](user/project/repository/index.md) | Manage source code repositories in GitLab's user interface. | +| [Repository mirroring](workflow/repository_mirroring.md) **[STARTER]** | Push to or pull from repositories outside of GitLab | | [Start a merge request](user/project/repository/web_editor.md#tips) | Start merge request when committing via GitLab's user interface. | <div align="right"> @@ -192,13 +204,14 @@ The following documentation relates to the DevOps **Create** stage: #### Integration and Automation -| Create Topics - Integration and Automation | Description | -|:------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------| -| [GitLab API](api/README.md) | Integrate GitLab via a simple and powerful API. | -| [GitLab Integration](integration/README.md) | Integrate with multiple third-party services with GitLab to allow external issue trackers and external authentication. | -| [GitLab Webhooks](user/project/integrations/webhooks.md) | Let GitLab notify you when new code has been pushed to your project. | -| [Project Services](user/project/integrations/project_services.md) | Integrate a project with external services, such as CI and chat. | -| [Trello Power-Up](integration/trello_power_up.md) | Integrate with GitLab's Trello Power-Up. | +| Create Topics - Integration and Automation | Description | +|:------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------| +| [GitLab API](api/README.md) | Integrate GitLab via a simple and powerful API. | +| [GitLab Integration](integration/README.md) | Integrate with multiple third-party services with GitLab to allow external issue trackers and external authentication. | +| [GitLab Webhooks](user/project/integrations/webhooks.md) | Let GitLab notify you when new code has been pushed to your project. | +| [JIRA Development Panel](integration/jira_development_panel.md) **[PREMIUM]** | See GitLab information in the JIRA Development Panel. | +| [Project Services](user/project/integrations/project_services.md) | Integrate a project with external services, such as CI and chat. | +| [Trello Power-Up](integration/trello_power_up.md) | Integrate with GitLab's Trello Power-Up. | <div align="right"> <a type="button" class="btn btn-default" href="#overview"> @@ -218,12 +231,14 @@ scales to run your tests faster. The following documentation relates to the DevOps **Verify** stage: -| Verify Topics | Description | -|:---------------------------------------------------------|:-----------------------------------------------------------------------------| -| [GitLab CI/CD](ci/README.md) | Explore the features and capabilities of Continuous Integration with GitLab. | -| [JUnit test reports](ci/junit_test_reports.md) | Display JUnit test reports on merge requests. | -| [Pipeline Graphs](ci/pipelines.md#visualizing-pipelines) | Visualize builds. | -| [Review Apps](ci/review_apps/index.md) | Preview changes to your application right from a merge request. | +| Verify Topics | Description | +|:----------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------| +| [Code Quality reports](user/project/merge_requests/code_quality.md) **[STARTER]** | Analyze source code quality. | +| [GitLab CI/CD](ci/README.md) | Explore the features and capabilities of Continuous Integration with GitLab. | +| [JUnit test reports](ci/junit_test_reports.md) | Display JUnit test reports on merge requests. | +| [Multi-project pipelines](ci/multi_project_pipelines.md) **[PREMIUM]** | Visualize entire pipelines that span multiple projects, including all cross-project inter-dependencies. | +| [Pipeline Graphs](ci/pipelines.md#visualizing-pipelines) | Visualize builds. | +| [Review Apps](ci/review_apps/index.md) | Preview changes to your application right from a merge request. | <div align="right"> <a type="button" class="btn btn-default" href="#overview"> @@ -242,6 +257,7 @@ The following documentation relates to the DevOps **Package** stage: | Package Topics | Description | |:----------------------------------------------------------------|:-------------------------------------------------------| | [GitLab Container Registry](user/project/container_registry.md) | Learn how to use GitLab's built-in Container Registry. | +| [GitLab Packages](administration/packages.md) **[PREMIUM]** | Use GitLab as an NPM registry or Maven repository. | <div align="right"> <a type="button" class="btn btn-default" href="#overview"> @@ -257,14 +273,17 @@ confidently and securely with GitLab’s built-in Continuous Delivery and Deploy The following documentation relates to the DevOps **Release** stage: -| Release Topics | Description | -|:------------------------------------------------------------|:---------------------------------------------------------------------------------------------| -| [Auto Deploy](topics/autodevops/index.md#auto-deploy) | Configure GitLab for the deployment of your application. | -| [Environments and deployments](ci/environments.md) | With environments, you can control the continuous deployment of your software within GitLab. | -| [GitLab CI/CD](ci/README.md) | Explore the features and capabilities of Continuous Deployment and Delivery with GitLab. | -| [GitLab Pages](user/project/pages/index.md) | Build, test, and deploy a static site directly from GitLab. | -| [Protected Runners](ci/runners/README.md#protected-runners) | Select Runners to only pick jobs for protected branches and tags. | -| [Scheduled Pipelines](user/project/pipelines/schedules.md) | Execute pipelines on a schedule. | +| Release Topics | Description | +|:------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------| +| [Auto Deploy](topics/autodevops/index.md#auto-deploy) | Configure GitLab for the deployment of your application. | +| [Canary Deployments](user/project/canary_deployments.md) **[PREMIUM]** | Employ a popular CI strategy where a small portion of the fleet is updated to the new version first. | +| [Deploy Boards](user/project/deploy_boards.md) **[PREMIUM]** | View the current health and status of each CI environment running on Kubernetes, displaying the status of the pods in the deployment. | +| [Environments and deployments](ci/environments.md) | With environments, you can control the continuous deployment of your software within GitLab. | +| [Environment-specific variables](ci/variables/README.md#limiting-environment-scopes-of-environment-variables-premium) **[PREMIUM]** | Limit scope of variables to specific environments. | +| [GitLab CI/CD](ci/README.md) | Explore the features and capabilities of Continuous Deployment and Delivery with GitLab. | +| [GitLab Pages](user/project/pages/index.md) | Build, test, and deploy a static site directly from GitLab. | +| [Protected Runners](ci/runners/README.md#protected-runners) | Select Runners to only pick jobs for protected branches and tags. | +| [Scheduled Pipelines](user/project/pipelines/schedules.md) | Execute pipelines on a schedule. | <div align="right"> <a type="button" class="btn btn-default" href="#overview"> @@ -288,6 +307,7 @@ The following documentation relates to the DevOps **Configure** stage: | [GitLab ChatOps](ci/chatops/README.md) | Interact with CI/CD jobs through chat services. | | [Installing Applications](user/project/clusters/index.md#installing-applications) | Deploy Helm, Ingress, and Prometheus on Kubernetes. | | [Mattermost slash commands](user/project/integrations/mattermost_slash_commands.md) | Enable and use slash commands from within Mattermost. | +| [Multiple Kubernetes Clusters](user/project/clusters/index.md#multiple-kubernetes-clusters-premium) **[PREMIUM]** | Associate more than one Kubernetes clusters to your project. | | [Protected variables](ci/variables/README.md#protected-environment-variables) | Restrict variables to protected branches and tags. | | [Serverless](user/project/clusters/serverless/index.md) | Run serverless workloads on Kubernetes. | | [Slack slash commands](user/project/integrations/slack_slash_commands.md) | Enable and use slash commands from within Slack. | @@ -323,16 +343,23 @@ The following documentation relates to the DevOps **Monitor** stage: ### Secure -GitLab can help you secure your applications from within your development lifecycle. +Check your application for security vulnerabilities that may lead to unauthorized access, +data leaks, and denial of services. GitLab will perform static and dynamic tests on the +code of your application, looking for known flaws and report them in the merge request +so you can fix them before merging. Security teams can use dashboards to get a +high-level view on projects and groups, and start remediation processes when needed. The following documentation relates to the DevOps **Secure** stage: -| Monitor Topics | Description | -|:----------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------| -| [Container Scanning example](ci/examples/container_scanning.md) | `.gitlab-ci.yml` example of using Clair and clair-scanner to scan docker images for known vulnerabilities. | - -NOTE: **Note:** -Viewing [Container Scanning reports](https://docs.gitlab.com/ee/user/project/merge_requests/container_scanning.html) within merge requests requires [GitLab Ultimate](https://about.gitlab.com/pricing/). +| Secure Topics | Description | +|:------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------| +| [Container Scanning](user/application_security/container_scanning/index.md) **[ULTIMATE]** | Use Clair to scan docker images for known vulnerabilities. | +| [Dependency Scanning](user/application_security/dependency_scanning/index.md) **[ULTIMATE]** | Analyze your dependencies for known vulnerabilities. | +| [Dynamic Application Security Testing (DAST)](user/application_security/dast/index.md) **[ULTIMATE]** | Analyze running web applications for known vulnerabilities. | +| [Group Security Dashboard](user/application_security/security_dashboard/index.md) **[ULTIMATE]** | View vulnerabilities in all the projects in a group and its subgroups. | +| [License Management](user/application_security/license_management/index.md) **[ULTIMATE]** | Search your project's dependencies for their licenses. | +| [Project Security Dashboard](user/application_security/security_dashboard/index.md) **[ULTIMATE]** | View the latest security reports for your project. | +| [Static Application Security Testing (SAST)](user/application_security/sast/index.md) **[ULTIMATE]** | Analyze source code for known vulnerabilities. | ## Subscribe to GitLab @@ -342,6 +369,8 @@ There are two ways to use GitLab: - [GitLab.com](#gitlabcom): GitLab's SaaS offering. You don't need to install anything to use GitLab.com, you only need to [sign up](https://gitlab.com/users/sign_in) and start using GitLab straight away. +For more information on managing your subscription and [Customers Portal](https://customers.gitlab.com) account, please see [Getting Started with Subscriptions](getting-started/subscription.md). + The following sections outline tiers and features within GitLab self-managed and GitLab.com. <div align="right"> @@ -393,6 +422,12 @@ GitLab.com subscriptions grant access to the same features available in GitLab self-managed, **except [administration](administration/index.md) tools and settings**. +GitLab.com allows you to apply your subscription to a group or your personal user. + +When applied to a **group**, the group, all subgroups, and all projects under the selected group on GitLab.com will have the features of the associated plan. It is recommended to go with a group plan when managing projects and users of an organization. + +When associated with a **personal userspace** instead, all projects will have features with the subscription applied, but as it is not a group, group features will not be available. + TIP: **Tip:** To support the open source community and encourage the development of open source projects, GitLab grants access to **Gold** features for all GitLab.com **public** projects, regardless of the subscription. diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md index 423a79c56d8..54279897e04 100644 --- a/doc/administration/auth/ldap.md +++ b/doc/administration/auth/ldap.md @@ -30,9 +30,9 @@ the LDAP server. ### User deletion -If a user is deleted from the LDAP server, they will be blocked in GitLab, as +If a user is deleted from the LDAP server, they will be blocked in GitLab as well. Users will be immediately blocked from logging in. However, there is an -LDAP check cache time (sync time) of one hour (see note). This means users that +LDAP check cache time of one hour (see note) which means users that are already logged in or are using Git over SSH will still be able to access GitLab for up to one hour. Manually block the user in the GitLab Admin area to immediately block all access. diff --git a/doc/administration/dependency_proxy.md b/doc/administration/dependency_proxy.md new file mode 100644 index 00000000000..4dc1f4dcba4 --- /dev/null +++ b/doc/administration/dependency_proxy.md @@ -0,0 +1,150 @@ +# GitLab Dependency Proxy administration **[PREMIUM ONLY]** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/7934) in [GitLab Premium](https://about.gitlab.com/pricing) 11.11. + +GitLab can be utilized as a dependency proxy for a variety of common package managers. + +This is the administration documentation. If you want to learn how to use the +dependency proxies, see the [user guide](../user/group/dependency_proxy/index.md). + +## Enabling the Dependency Proxy feature + +NOTE: **Note:** +Dependency proxy requires the Puma web server to be enabled. +Puma support is EXPERIMENTAL at this time. + +To enable the Dependency proxy feature: + +**Omnibus GitLab installations** + +1. Edit `/etc/gitlab/gitlab.rb` and add the following line: + + ```ruby + gitlab_rails['dependency_proxy_enabled'] = true + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. +1. Enable the [Puma web server](https://docs.gitlab.com/omnibus/settings/puma.html). + +**Installations from source** + +1. After the installation is complete, you will have to configure the `dependency_proxy` + section in `config/gitlab.yml`. Set to `true` to enable it: + + ```yaml + dependency_proxy: + enabled: true + ``` + +1. [Restart GitLab] for the changes to take effect. +1. Enable the [Puma web server](../install/installation.md#using-puma). + +## Changing the storage path + +By default, the dependency proxy files are stored locally, but you can change the default +local location or even use object storage. + +### Changing the local storage path + +The dependency proxy files for Omnibus GitLab installations are stored under +`/var/opt/gitlab/gitlab-rails/shared/dependency_proxy/` and for source +installations under `shared/dependency_proxy/` (relative to the git home directory). +To change the local storage path: + +**Omnibus GitLab installations** + +1. Edit `/etc/gitlab/gitlab.rb` and add the following line: + + ```ruby + gitlab_rails['dependency_proxy_storage_path'] = "/mnt/dependency_proxy" + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + +**Installations from source** + +1. Edit the `dependency_proxy` section in `config/gitlab.yml`: + + ```yaml + dependency_proxy: + enabled: true + storage_path: shared/dependency_proxy + ``` +1. [Restart GitLab] for the changes to take effect. + +### Using object storage + +Instead of relying on the local storage, you can use an object storage to +upload the blobs of the dependency proxy: + +**Omnibus GitLab installations** + +1. Edit `/etc/gitlab/gitlab.rb` and add the following lines (uncomment where + necessary): + + ```ruby + gitlab_rails['dependency_proxy_enabled'] = true + gitlab_rails['dependency_proxy_storage_path'] = "/var/opt/gitlab/gitlab-rails/shared/dependency_proxy" + gitlab_rails['dependency_proxy_object_store_enabled'] = true + gitlab_rails['dependency_proxy_object_store_remote_directory'] = "dependency_proxy" # The bucket name. + gitlab_rails['dependency_proxy_object_store_direct_upload'] = false # Use Object Storage directly for uploads instead of background uploads if enabled (Default: false). + gitlab_rails['dependency_proxy_object_store_background_upload'] = true # Temporary option to limit automatic upload (Default: true). + gitlab_rails['dependency_proxy_object_store_proxy_download'] = false # Passthrough all downloads via GitLab instead of using Redirects to Object Storage. + gitlab_rails['dependency_proxy_object_store_connection'] = { + ## + ## If the provider is AWS S3, uncomment the following + ## + #'provider' => 'AWS', + #'region' => 'eu-west-1', + #'aws_access_key_id' => 'AWS_ACCESS_KEY_ID', + #'aws_secret_access_key' => 'AWS_SECRET_ACCESS_KEY', + ## + ## If the provider is other than AWS (an S3-compatible one), uncomment the following + ## + #'host' => 's3.amazonaws.com', + #'aws_signature_version' => 4 # For creation of signed URLs. Set to 2 if provider does not support v4. + #'endpoint' => 'https://s3.amazonaws.com' # Useful for S3-compliant services such as DigitalOcean Spaces. + #'path_style' => false # If true, use 'host/bucket_name/object' instead of 'bucket_name.host/object'. + } + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + +**Installations from source** + +1. Edit the `dependency_proxy` section in `config/gitlab.yml` (uncomment where necessary): + + ```yaml + dependency_proxy: + enabled: true + ## + ## The location where build dependency_proxy are stored (default: shared/dependency_proxy). + ## + #storage_path: shared/dependency_proxy + object_store: + enabled: false + remote_directory: dependency_proxy # The bucket name. + #direct_upload: false # Use Object Storage directly for uploads instead of background uploads if enabled (Default: false). + #background_upload: true # Temporary option to limit automatic upload (Default: true). + #proxy_download: false # Passthrough all downloads via GitLab instead of using Redirects to Object Storage. + connection: + ## + ## If the provider is AWS S3, uncomment the following + ## + #provider: AWS + #region: us-east-1 + #aws_access_key_id: AWS_ACCESS_KEY_ID + #aws_secret_access_key: AWS_SECRET_ACCESS_KEY + ## + ## If the provider is other than AWS (an S3-compatible one), uncomment the following + ## + #host: 's3.amazonaws.com' # default: s3.amazonaws.com. + #aws_signature_version: 4 # For creation of signed URLs. Set to 2 if provider does not support v4. + #endpoint: 'https://s3.amazonaws.com' # Useful for S3-compliant services such as DigitalOcean Spaces. + #path_style: false # If true, use 'host/bucket_name/object' instead of 'bucket_name.host/object'. + ``` + +1. [Restart GitLab] for the changes to take effect. + +[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab" +[restart gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab" diff --git a/doc/administration/geo/disaster_recovery/background_verification.md b/doc/administration/geo/disaster_recovery/background_verification.md index 7d2fd51f834..c7299b6e196 100644 --- a/doc/administration/geo/disaster_recovery/background_verification.md +++ b/doc/administration/geo/disaster_recovery/background_verification.md @@ -157,6 +157,42 @@ For wikis: sudo -u git -H bundle exec rake geo:verification:wiki:reset RAILS_ENV=production ``` +## Reconcile differences with checksum mismatches + +If the **primary** and **secondary** nodes have a checksum verification mismatch, the cause may not be apparent. To find the cause of a checksum mismatch: + +1. Navigate to the **Admin Area > Projects** dashboard on the **primary** node, find the + project that you want to check the checksum differences and click on the + **Edit** button: + ![Projects dashboard](img/checksum-differences-admin-projects.png) + +1. On the project admin page get the **Gitaly storage name**, and **Gitaly relative path**: + ![Project admin page](img/checksum-differences-admin-project-page.png) + +1. Navigate to the project's repository directory on both **primary** and **secondary** nodes. For an installation from source, the path is usually `/home/git/repositories`. For Omnibus installs, the path is usually `/var/opt/gitlab/git-data/repositories`. Note that if `git_data_dirs` is customized, check the directory layout on your server to be sure. + + ```sh + cd /var/opt/gitlab/git-data/repositories + ``` + +1. Run the following command on the **primary** node, redirecting the output to a file: + + ```sh + git show-ref --head | grep -E "HEAD|(refs/(heads|tags|keep-around|merge-requests|environments|notes)/)" > primary-node-refs + ``` + +1. Run the following command on the **secondary** node, redirecting the output to a file: + + ```sh + git show-ref --head | grep -E "HEAD|(refs/(heads|tags|keep-around|merge-requests|environments|notes)/)" > secondary-node-refs + ``` + +1. Copy the files from the previous steps on the same system, and do a diff between the contents: + + ```sh + diff primary-node-refs secondary-node-refs + ``` + ## Current limitations Until [issue #5064][ee-5064] is completed, background verification doesn't cover diff --git a/doc/administration/geo/disaster_recovery/img/checksum-differences-admin-project-page.png b/doc/administration/geo/disaster_recovery/img/checksum-differences-admin-project-page.png Binary files differnew file mode 100644 index 00000000000..fd51523104b --- /dev/null +++ b/doc/administration/geo/disaster_recovery/img/checksum-differences-admin-project-page.png diff --git a/doc/administration/geo/disaster_recovery/img/checksum-differences-admin-projects.png b/doc/administration/geo/disaster_recovery/img/checksum-differences-admin-projects.png Binary files differnew file mode 100644 index 00000000000..b2a6da69d3d --- /dev/null +++ b/doc/administration/geo/disaster_recovery/img/checksum-differences-admin-projects.png diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md index de61c01991b..24db1c28778 100644 --- a/doc/administration/high_availability/README.md +++ b/doc/administration/high_availability/README.md @@ -145,14 +145,38 @@ environments this is a good architecture to consider if you foresee or do have contention due to certain workloads. - 3 PostgreSQL nodes +- 1 PgBouncer node - 2 Redis nodes - 3 Consul/Sentinel nodes - 2 or more Sidekiq nodes -- 2 or more Web nodes (Unicorn, Workhorse, PGBouncer) +- 2 or more GitLab application nodes (Unicorn, Workhorse) - 1 or more NFS/Gitaly servers +- 1 Monitoring node (Prometheus, Grafana) ![Hybrid architecture diagram](https://docs.gitlab.com/ee/administration/img/high_availability/hybrid.png) +#### Reference Architecture + +- **Status:** Work-in-progress +- **Supported Users (approximate):** 10,000 +- **Related Issues:** [gitlab-com/support/support-team-meta#1513](https://gitlab.com/gitlab-com/support/support-team-meta/issues/1513), + [gitlab-org/quality/team-tasks#110](https://gitlab.com/gitlab-org/quality/team-tasks/issues/110) + +The Support and Quality teams are in the process of building and performance testing +an environment that will support about 10,000 users. The specifications below +are a work-in-progress representation of the work so far. Quality will be +certifying this environment in FY20-Q2. The specifications may be adjusted +prior to certification based on performance testing. + +- 3 PostgreSQL - 4 CPU, 8GB RAM +- 1 PgBouncer - 2 CPU, 4GB RAM +- 2 Redis - 2 CPU, 8GB RAM +- 3 Consul/Sentinel - 2 CPU, 2GB RAM +- 4 Sidekiq - 4 CPU, 8GB RAM +- 5 GitLab application nodes - 20 CPU, 64GB RAM +- 1 Gitaly - 20 CPU, 64GB RAM +- 1 Monitoring node - 4 CPU, 8GB RAM + ### Fully Distributed This architecture scales to hundreds of thousands of users and projects and is diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md index d0e0e320019..caadec3ac4e 100644 --- a/doc/administration/high_availability/nfs.md +++ b/doc/administration/high_availability/nfs.md @@ -41,7 +41,6 @@ options: NOTE: **Note:** This is only available starting in certain versions of GitLab: - * 11.5.11 * 11.6.11 * 11.7.12 @@ -59,13 +58,13 @@ details. To do this, run the Rake task: ```sh -gitlab-rake gitlab:features:enable_rugged +sudo gitlab-rake gitlab:features:enable_rugged ``` If you need to undo this setting for some reason, run: ```sh -gitlab-rake gitlab:features:disable_rugged +sudo gitlab-rake gitlab:features:disable_rugged ``` ### Known issues diff --git a/doc/administration/high_availability/nfs_host_client_setup.md b/doc/administration/high_availability/nfs_host_client_setup.md index a8bc101dee6..a8d69b9ab0a 100644 --- a/doc/administration/high_availability/nfs_host_client_setup.md +++ b/doc/administration/high_availability/nfs_host_client_setup.md @@ -67,7 +67,7 @@ apt-get install nfs-common ### Step 2 - Create Mount Points on Client -Create a directroy on the client that we can mount the shared directory from the host. +Create a directory on the client that we can mount the shared directory from the host. Please note that if your mount point directory contains any files they will be hidden once the remote shares are mounted. An empty/new directory on the client is recommended for this purpose. diff --git a/doc/administration/img/high_availability/fully-distributed.png b/doc/administration/img/high_availability/fully-distributed.png Binary files differdeleted file mode 100644 index ad23207134e..00000000000 --- a/doc/administration/img/high_availability/fully-distributed.png +++ /dev/null diff --git a/doc/administration/img/high_availability/geo-ha-diagram.png b/doc/administration/img/high_availability/geo-ha-diagram.png Binary files differdeleted file mode 100644 index da5d612827c..00000000000 --- a/doc/administration/img/high_availability/geo-ha-diagram.png +++ /dev/null diff --git a/doc/administration/img/high_availability/horizontal.png b/doc/administration/img/high_availability/horizontal.png Binary files differdeleted file mode 100644 index c3bd489d96f..00000000000 --- a/doc/administration/img/high_availability/horizontal.png +++ /dev/null diff --git a/doc/administration/img/high_availability/hybrid.png b/doc/administration/img/high_availability/hybrid.png Binary files differdeleted file mode 100644 index 7d4a56bf0ea..00000000000 --- a/doc/administration/img/high_availability/hybrid.png +++ /dev/null diff --git a/doc/administration/index.md b/doc/administration/index.md index 02e88dbd2a6..797a7242bd0 100644 --- a/doc/administration/index.md +++ b/doc/administration/index.md @@ -40,6 +40,7 @@ Learn how to install, configure, update, and maintain your GitLab instance. - [Geo](https://docs.gitlab.com/ee/administration/geo/replication/index.html): Replicate your GitLab instance to other geographic locations as a read-only fully operational version. **[PREMIUM ONLY]** - [Disaster Recovery](https://docs.gitlab.com/ee/administration/geo/disaster_recovery/index.html): Quickly fail-over to a different site with minimal effort in a disaster situation. **[PREMIUM ONLY]** - [Pivotal Tile](https://docs.gitlab.com/ee/install/pivotal/index.html): Deploy GitLab as a pre-configured appliance using Ops Manager (BOSH) for Pivotal Cloud Foundry. **[PREMIUM ONLY]** +- [Add License](../user/admin_area/license.md): Upload a license at install time to unlock features that are in paid tiers of GitLab. **[STARTER ONLY]** ### Configuring GitLab diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md index d383d1efe70..b7b820abb40 100644 --- a/doc/administration/integration/plantuml.md +++ b/doc/administration/integration/plantuml.md @@ -57,8 +57,7 @@ you can change these defaults by editing the `/etc/tomcat7/server.xml` file. You need to enable PlantUML integration from Settings under Admin Area. To do that, login with an Admin account and do following: - - in GitLab go to **Admin Area** and then **Settings** - - scroll to bottom of the page until PlantUML section + - in GitLab go to **Admin Area**->**Settings**->**Integrations**->**PlantUML** - check **Enable PlantUML** checkbox - set the PlantUML instance as **PlantUML URL** diff --git a/doc/api/discussions.md b/doc/api/discussions.md index 67bbd4cc1ac..07a6201b10b 100644 --- a/doc/api/discussions.md +++ b/doc/api/discussions.md @@ -153,7 +153,8 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab ### Add note to existing issue discussion -Adds a new note to the discussion. +Adds a new note to the discussion. This can also +[create a discussion from a single comment](../user/discussions/#start-a-discussion-by-replying-to-a-standard-comment). ``` POST /projects/:id/issues/:issue_iid/discussions/:discussion_id/notes @@ -652,7 +653,8 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab. ### Add note to existing merge request discussion -Adds a new note to the discussion. +Adds a new note to the discussion. This can also +[create a discussion from a single comment](../user/discussions/#start-a-discussion-by-replying-to-a-standard-comment). ``` POST /projects/:id/merge_requests/:merge_request_iid/discussions/:discussion_id/notes diff --git a/doc/api/epic_issues.md b/doc/api/epic_issues.md new file mode 100644 index 00000000000..438a3361dcc --- /dev/null +++ b/doc/api/epic_issues.md @@ -0,0 +1,412 @@ +# Epic Issues API **[ULTIMATE]** + +Every API call to epic_issues must be authenticated. + +If a user is not a member of a group and the group is private, a `GET` request on that group will result to a `404` status code. + +Epics are available only in Ultimate. If epics feature is not available a `403` status code will be returned. + +## List issues for an epic +Gets all issues that are assigned to an epic and the authenticated user has access to. + +``` +GET /groups/:id/epics/:epic_iid/issues +``` + +| Attribute | Type | Required | Description | +| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | +| `epic_iid` | integer/string | yes | The internal ID of the epic. | + +```bash +curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/5/issues/ +``` + +Example response: + +```json +[ + { + "id": 76, + "iid": 6, + "project_id": 8, + "title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.", + "description" : "Ratione dolores corrupti mollitia soluta quia.", + "state": "opened", + "created_at": "2017-11-15T13:39:24.670Z", + "updated_at": "2018-01-04T10:49:19.506Z", + "closed_at": null, + "labels": [], + "milestone": { + "id": 38, + "iid": 3, + "project_id": 8, + "title": "v2.0", + "description": "In tempore culpa inventore quo accusantium.", + "state": "closed", + "created_at": "2017-11-15T13:39:13.825Z", + "updated_at": "2017-11-15T13:39:13.825Z", + "due_date": null, + "start_date": null + }, + "assignees": [{ + "id": 7, + "name": "Pamella Huel", + "username": "arnita", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/a2f5c6fcef64c9c69cb8779cb292be1b?s=80&d=identicon", + "web_url": "http://localhost:3001/arnita" + }], + "assignee": { + "id": 7, + "name": "Pamella Huel", + "username": "arnita", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/a2f5c6fcef64c9c69cb8779cb292be1b?s=80&d=identicon", + "web_url": "http://localhost:3001/arnita" + }, + "author": { + "id": 13, + "name": "Michell Johns", + "username": "chris_hahn", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/30e3b2122ccd6b8e45e8e14a3ffb58fc?s=80&d=identicon", + "web_url": "http://localhost:3001/chris_hahn" + }, + "user_notes_count": 8, + "upvotes": 0, + "downvotes": 0, + "due_date": null, + "confidential": false, + "weight": null, + "discussion_locked": null, + "web_url": "http://localhost:3001/h5bp/html5-boilerplate/issues/6", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null + }, + "_links":{ + "self": "http://localhost:3001/api/v4/projects/8/issues/6", + "notes": "http://localhost:3001/api/v4/projects/8/issues/6/notes", + "award_emoji": "http://localhost:3001/api/v4/projects/8/issues/6/award_emoji", + "project": "http://localhost:3001/api/v4/projects/8" + }, + "subscribed": true, + "epic_issue_id": 2 + } +] +``` + +**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API. + +## Assign an issue to the epic + +Creates an epic - issue association. If the issue in question belongs to another epic it is unassigned from that epic. + +``` +POST /groups/:id/epics/:epic_iid/issues/:issue_id +``` + +| Attribute | Type | Required | Description | +| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | +| `epic_iid` | integer/string | yes | The internal ID of the epic. | +| `issue_id` | integer/string | yes | The ID of the issue. | + +```bash +curl --header POST "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/5/issues/55 +``` + +Example response: + +```json +{ + "id": 11, + "epic": { + "id": 30, + "iid": 5, + "title": "Ea cupiditate dolores ut vero consequatur quasi veniam voluptatem et non.", + "description": "Molestias dolorem eos vitae expedita impedit necessitatibus quo voluptatum.", + "author": { + "id": 7, + "name": "Pamella Huel", + "username": "arnita", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/a2f5c6fcef64c9c69cb8779cb292be1b?s=80&d=identicon", + "web_url": "http://localhost:3001/arnita" + }, + "start_date": null, + "end_date": null + }, + "issue": { + "id": 55, + "iid": 13, + "project_id": 8, + "title": "Beatae laborum voluptatem voluptate eligendi ex accusamus.", + "description": "Quam veritatis debitis omnis aliquam sit.", + "state": "opened", + "created_at": "2017-11-05T13:59:12.782Z", + "updated_at": "2018-01-05T10:33:03.900Z", + "closed_at": null, + "labels": [], + "milestone": { + "id": 48, + "iid": 6, + "project_id": 8, + "title": "Sprint - Sed sed maxime temporibus ipsa ullam qui sit.", + "description": "Quos veritatis qui expedita sunt deleniti accusamus.", + "state": "active", + "created_at": "2017-11-05T13:59:12.445Z", + "updated_at": "2017-11-05T13:59:12.445Z", + "due_date": "2017-11-13", + "start_date": "2017-11-05" + }, + "assignees": [{ + "id": 10, + "name": "Lu Mayer", + "username": "kam", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/018729e129a6f31c80a6327a30196823?s=80&d=identicon", + "web_url": "http://localhost:3001/kam" + }], + "assignee": { + "id": 10, + "name": "Lu Mayer", + "username": "kam", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/018729e129a6f31c80a6327a30196823?s=80&d=identicon", + "web_url": "http://localhost:3001/kam" + }, + "author": { + "id": 25, + "name": "User 3", + "username": "user3", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/97d6d9441ff85fdc730e02a6068d267b?s=80&d=identicon", + "web_url": "http://localhost:3001/user3" + }, + "user_notes_count": 0, + "upvotes": 0, + "downvotes": 0, + "due_date": null, + "confidential": false, + "weight": null, + "discussion_locked": null, + "web_url": "http://localhost:3001/h5bp/html5-boilerplate/issues/13", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null + } + } +} +``` + +**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API. + +## Remove an issue from the epic + +Removes an epic - issue association. + +``` +DELETE /groups/:id/epics/:epic_iid/issues/:epic_issue_id +``` + +| Attribute | Type | Required | Description | +| ------------------- | ---------------- | ---------- | -----------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | +| `epic_iid` | integer/string | yes | The internal ID of the epic. | +| `epic_issue_id` | integer/string | yes | The ID of the issue - epic association. | + +```bash +curl --header DELETE "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/5/issues/11 +``` + +Example response: + +```json +{ + "id": 11, + "epic": { + "id": 30, + "iid": 5, + "title": "Ea cupiditate dolores ut vero consequatur quasi veniam voluptatem et non.", + "description": "Molestias dolorem eos vitae expedita impedit necessitatibus quo voluptatum.", + "author": { + "id": 7, + "name": "Pamella Huel", + "username": "arnita", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/a2f5c6fcef64c9c69cb8779cb292be1b?s=80&d=identicon", + "web_url": "http://localhost:3001/arnita" + }, + "start_date": null, + "end_date": null + }, + "issue": { + "id": 223, + "iid": 13, + "project_id": 8, + "title": "Beatae laborum voluptatem voluptate eligendi ex accusamus.", + "description": "Quam veritatis debitis omnis aliquam sit.", + "state": "opened", + "created_at": "2017-11-05T13:59:12.782Z", + "updated_at": "2018-01-05T10:33:03.900Z", + "closed_at": null, + "labels": [], + "milestone": { + "id": 48, + "iid": 6, + "project_id": 8, + "title": "Sprint - Sed sed maxime temporibus ipsa ullam qui sit.", + "description": "Quos veritatis qui expedita sunt deleniti accusamus.", + "state": "active", + "created_at": "2017-11-05T13:59:12.445Z", + "updated_at": "2017-11-05T13:59:12.445Z", + "due_date": "2017-11-13", + "start_date": "2017-11-05" + }, + "assignees": [{ + "id": 10, + "name": "Lu Mayer", + "username": "kam", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/018729e129a6f31c80a6327a30196823?s=80&d=identicon", + "web_url": "http://localhost:3001/kam" + }], + "assignee": { + "id": 10, + "name": "Lu Mayer", + "username": "kam", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/018729e129a6f31c80a6327a30196823?s=80&d=identicon", + "web_url": "http://localhost:3001/kam" + }, + "author": { + "id": 25, + "name": "User 3", + "username": "user3", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/97d6d9441ff85fdc730e02a6068d267b?s=80&d=identicon", + "web_url": "http://localhost:3001/user3" + }, + "user_notes_count": 0, + "upvotes": 0, + "downvotes": 0, + "due_date": null, + "confidential": false, + "weight": null, + "discussion_locked": null, + "web_url": "http://localhost:3001/h5bp/html5-boilerplate/issues/13", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null + } + } +} +``` + +**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API. + +## Update epic - issue association + +Updates an epic - issue association. + +``` +PUT /groups/:id/epics/:epic_iid/issues/:epic_issue_id +``` + +| Attribute | Type | Required | Description | +| ------------------- | ---------------- | ---------- | -----------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | +| `epic_iid` | integer/string | yes | The internal ID of the epic. | +| `epic_issue_id` | integer/string | yes | The ID of the issue - epic association. | +| `move_before_id` | integer/string | no | The ID of the issue - epic association that should be placed before the link in the question. | +| `move_after_id` | integer/string | no | The ID of the issue - epic association that should be placed after the link in the question. | + +```bash +curl --header PUT "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/5/issues/11?move_before_id=20 +``` + +Example response: + +```json +[ + { + "id": 30, + "iid": 6, + "project_id": 8, + "title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.", + "description" : "Ratione dolores corrupti mollitia soluta quia.", + "state": "opened", + "created_at": "2017-11-15T13:39:24.670Z", + "updated_at": "2018-01-04T10:49:19.506Z", + "closed_at": null, + "labels": [], + "milestone": { + "id": 38, + "iid": 3, + "project_id": 8, + "title": "v2.0", + "description": "In tempore culpa inventore quo accusantium.", + "state": "closed", + "created_at": "2017-11-15T13:39:13.825Z", + "updated_at": "2017-11-15T13:39:13.825Z", + "due_date": null, + "start_date": null + }, + "assignees": [{ + "id": 7, + "name": "Pamella Huel", + "username": "arnita", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/a2f5c6fcef64c9c69cb8779cb292be1b?s=80&d=identicon", + "web_url": "http://localhost:3001/arnita" + }], + "assignee": { + "id": 7, + "name": "Pamella Huel", + "username": "arnita", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/a2f5c6fcef64c9c69cb8779cb292be1b?s=80&d=identicon", + "web_url": "http://localhost:3001/arnita" + }, + "author": { + "id": 13, + "name": "Michell Johns", + "username": "chris_hahn", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/30e3b2122ccd6b8e45e8e14a3ffb58fc?s=80&d=identicon", + "web_url": "http://localhost:3001/chris_hahn" + }, + "user_notes_count": 8, + "upvotes": 0, + "downvotes": 0, + "due_date": null, + "confidential": false, + "weight": null, + "discussion_locked": null, + "web_url": "http://localhost:3001/h5bp/html5-boilerplate/issues/6", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null + }, + "_links":{ + "self": "http://localhost:3001/api/v4/projects/8/issues/6", + "notes": "http://localhost:3001/api/v4/projects/8/issues/6/notes", + "award_emoji": "http://localhost:3001/api/v4/projects/8/issues/6/award_emoji", + "project": "http://localhost:3001/api/v4/projects/8" + }, + "subscribed": true, + "epic_issue_id": 11, + "relative_position": 55 + } +] +``` diff --git a/doc/api/epic_links.md b/doc/api/epic_links.md new file mode 100644 index 00000000000..619ae6ea2dc --- /dev/null +++ b/doc/api/epic_links.md @@ -0,0 +1,252 @@ +# Epic Links API **[ULTIMATE]** + +>**Note:** +> This endpoint was [introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/9188) in GitLab 11.8. + +Manages parent-child [epic relationships](../user/group/epics/index.md#multi-level-child-epics). + +Every API call to `epic_links` must be authenticated. + +If a user is not a member of a group and the group is private, a `GET` request on that group will result to a `404` status code. + +Epics are available only in the [Ultimate/Gold tier](https://about.gitlab.com/pricing/). If the epics feature is not available, a `403` status code will be returned. + +## List epics related to a given epic +Gets all child epics of an epic. + +``` +GET /groups/:id/epics/:epic_iid/epics +``` + +| Attribute | Type | Required | Description | +| ---------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | +| `epic_iid` | integer | yes | The internal ID of the epic. | + +```bash +curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/5/epics/ +``` + +Example response: + +```json +[ + { + "id": 29, + "iid": 6, + "group_id": 1, + "parent_id": 5, + "title": "Accusamus iste et ullam ratione voluptatem omnis debitis dolor est.", + "description": "Molestias dolorem eos vitae expedita impedit necessitatibus quo voluptatum.", + "author": { + "id": 10, + "name": "Lu Mayer", + "username": "kam", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/018729e129a6f31c80a6327a30196823?s=80&d=identicon", + "web_url": "http://localhost:3001/kam" + }, + "start_date": null, + "start_date_is_fixed": false, + "start_date_fixed": null, + "start_date_from_milestones": null, + "end_date": "2018-07-31", + "due_date": "2018-07-31", + "due_date_is_fixed": false, + "due_date_fixed": null, + "due_date_from_milestones": "2018-07-31", + "created_at": "2018-07-17T13:36:22.770Z", + "updated_at": "2018-07-18T12:22:05.239Z", + "labels": [] + } +] +``` + +## Assign a child epic + +Creates an association between two epics, designating one as the parent epic and the other as the child epic. A parent epic can have multiple child epics. If the new child epic already belonged to another epic, it is unassigned from that previous parent. + +``` +POST /groups/:id/epics/:epic_iid/epics +``` + +| Attribute | Type | Required | Description | +| --------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------ | +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | +| `epic_iid` | integer | yes | The internal ID of the epic. | +| `child_epic_id` | integer | yes | The global ID of the child epic. Internal ID can't be used because they can conflict with epics from other groups. | + +```bash +curl --header POST "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/5/epics/6 +``` + +Example response: + +```json +{ + "id": 6, + "iid": 38, + "group_id": 1, + "parent_id": 5 + "title": "Accusamus iste et ullam ratione voluptatem omnis debitis dolor est.", + "description": "Molestias dolorem eos vitae expedita impedit necessitatibus quo voluptatum.", + "author": { + "id": 10, + "name": "Lu Mayer", + "username": "kam", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/018729e129a6f31c80a6327a30196823?s=80&d=identicon", + "web_url": "http://localhost:3001/kam" + }, + "start_date": null, + "start_date_is_fixed": false, + "start_date_fixed": null, + "start_date_from_milestones": null, + "end_date": "2018-07-31", + "due_date": "2018-07-31", + "due_date_is_fixed": false, + "due_date_fixed": null, + "due_date_from_milestones": "2018-07-31", + "created_at": "2018-07-17T13:36:22.770Z", + "updated_at": "2018-07-18T12:22:05.239Z", + "labels": [] +} +``` + +## Create and assign a child epic + +Creates a a new epic and associates it with provided parent epic. The response is LinkedEpic object. + +``` +POST /groups/:id/epics/:epic_iid/epics +``` + +| Attribute | Type | Required | Description | +| --------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------ | +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | +| `epic_iid` | integer | yes | The internal ID of the (future parent) epic. | +| `title` | integer | yes | The global ID of the child epic. Internal ID can't be used because they can conflict with epics from other groups. | + +```bash +curl --header POST "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/5/epics?title=Newpic +``` + +Example response: + +```json +{ + "id": 24, + "iid": 2, + "title": "child epic", + "group_id": 49, + "parent_id": 23, + "has_children": false, + "reference": "&2", + "url": "http://localhost/groups/group16/-/epics/2", + "relation_url": "http://localhost/groups/group16/-/epics/1/links/24" +} +``` + +## Re-order a child epic + +``` +PUT /groups/:id/epics/:epic_iid/epics/:child_epic_id +``` + +| Attribute | Type | Required | Description | +| ---------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------ | +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `epic_iid` | integer | yes | The internal ID of the epic. | +| `child_epic_id` | integer | yes | The global ID of the child epic. Internal ID can't be used because they can conflict with epics from other groups. | +| `move_before_id` | integer | no | The global ID of a sibling epic that should be placed before the child epic. | +| `move_after_id` | integer | no | The global ID of a sibling epic that should be placed after the child epic. | + +```bash +curl --header PUT "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/4/epics/5 +``` + +Example response: + +```json +[ + { + "id": 29, + "iid": 6, + "group_id": 1, + "parent_id": 5, + "title": "Accusamus iste et ullam ratione voluptatem omnis debitis dolor est.", + "description": "Molestias dolorem eos vitae expedita impedit necessitatibus quo voluptatum.", + "author": { + "id": 10, + "name": "Lu Mayer", + "username": "kam", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/018729e129a6f31c80a6327a30196823?s=80&d=identicon", + "web_url": "http://localhost:3001/kam" + }, + "start_date": null, + "start_date_is_fixed": false, + "start_date_fixed": null, + "start_date_from_milestones": null, + "end_date": "2018-07-31", + "due_date": "2018-07-31", + "due_date_is_fixed": false, + "due_date_fixed": null, + "due_date_from_milestones": "2018-07-31", + "created_at": "2018-07-17T13:36:22.770Z", + "updated_at": "2018-07-18T12:22:05.239Z", + "labels": [] + } +] +``` + +## Unassign a child epic + +Unassigns a child epic from a parent epic. + +``` +DELETE /groups/:id/epics/:epic_iid/epics/:child_epic_id +``` + +| Attribute | Type | Required | Description | +| --------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------ | +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `epic_iid` | integer | yes | The internal ID of the epic. | +| `child_epic_id` | integer | yes | The global ID of the child epic. Internal ID can't be used because they can conflict with epics from other groups. | + +```bash +curl --header DELETE "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/4/epics/5 +``` + +Example response: + +```json +{ + "id": 5, + "iid": 38, + "group_id": 1, + "parent_id": null, + "title": "Accusamus iste et ullam ratione voluptatem omnis debitis dolor est.", + "description": "Molestias dolorem eos vitae expedita impedit necessitatibus quo voluptatum.", + "author": { + "id": 10, + "name": "Lu Mayer", + "username": "kam", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/018729e129a6f31c80a6327a30196823?s=80&d=identicon", + "web_url": "http://localhost:3001/kam" + }, + "start_date": null, + "start_date_is_fixed": false, + "start_date_fixed": null, + "start_date_from_milestones": null, + "end_date": "2018-07-31", + "due_date": "2018-07-31", + "due_date_is_fixed": false, + "due_date_fixed": null, + "due_date_from_milestones": "2018-07-31", + "created_at": "2018-07-17T13:36:22.770Z", + "updated_at": "2018-07-18T12:22:05.239Z", + "labels": [] +} +``` diff --git a/doc/api/epics.md b/doc/api/epics.md new file mode 100644 index 00000000000..0541cfaa715 --- /dev/null +++ b/doc/api/epics.md @@ -0,0 +1,360 @@ +# Epics API **[ULTIMATE]** + +Every API call to epic must be authenticated. + +If a user is not a member of a group and the group is private, a `GET` request on that group will result to a `404` status code. + +If epics feature is not available a `403` status code will be returned. + +## Epic issues API + +The [epic issues API](epic_issues.md) allows you to interact with issues associated with an epic. + +# Milestone dates integration + +> [Introduced][ee-6448] in GitLab 11.3. + +Since start date and due date can be dynamically sourced from related issue milestones, when user has edit permission, additional fields will be shown. These include two boolean fields `start_date_is_fixed` and `due_date_is_fixed`, and four date fields `start_date_fixed`, `start_date_from_milestones`, `due_date_fixed` and `due_date_from_milestones`. + +`end_date` has been deprecated in favor of `due_date`. + +## Epics pagination + +By default, `GET` requests return 20 results at a time because the API results +are paginated. + +Read more on [pagination](README.md#pagination). + +## List epics for a group + +Gets all epics of the requested group and its subgroups. + +``` +GET /groups/:id/epics +GET /groups/:id/epics?author_id=5 +GET /groups/:id/epics?labels=bug,reproduced +GET /groups/:id/epics?state=opened +``` + +| Attribute | Type | Required | Description | +| ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | +| `author_id` | integer | no | Return epics created by the given user `id` | +| `labels` | string | no | Return epics matching a comma separated list of labels names. Label names from the epic group or a parent group can be used | +| `order_by` | string | no | Return epics ordered by `created_at` or `updated_at` fields. Default is `created_at` | +| `sort` | string | no | Return epics sorted in `asc` or `desc` order. Default is `desc` | +| `search` | string | no | Search epics against their `title` and `description` | +| `state` | string | no | Search epics against their `state`, possible filters: `opened`, `closed` and `all`, default: `all` | +| `created_after` | datetime | no | Return epics created on or after the given time | +| `created_before` | datetime | no | Return epics created on or before the given time | +| `updated_after` | datetime | no | Return epics updated on or after the given time | +| `updated_before` | datetime | no | Return epics updated on or before the given time | + +```bash +curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics +``` + +Example response: + +```json +[ + { + "id": 29, + "iid": 4, + "group_id": 7, + "title": "Accusamus iste et ullam ratione voluptatem omnis debitis dolor est.", + "description": "Molestias dolorem eos vitae expedita impedit necessitatibus quo voluptatum.", + "state": "opened", + "author": { + "id": 10, + "name": "Lu Mayer", + "username": "kam", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/018729e129a6f31c80a6327a30196823?s=80&d=identicon", + "web_url": "http://localhost:3001/kam" + }, + "start_date": null, + "start_date_is_fixed": false, + "start_date_fixed": null, + "start_date_from_milestones": null, + "end_date": "2018-07-31", + "due_date": "2018-07-31", + "due_date_is_fixed": false, + "due_date_fixed": null, + "due_date_from_milestones": "2018-07-31", + "created_at": "2018-07-17T13:36:22.770Z", + "updated_at": "2018-07-18T12:22:05.239Z", + "labels": [], + "upvotes": 4, + "downvotes": 0 + } +] +``` + +## Single epic + +Gets a single epic + +``` +GET /groups/:id/epics/:epic_iid +``` + +| Attribute | Type | Required | Description | +| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | +| `epic_iid` | integer/string | yes | The internal ID of the epic. | + +```bash +curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/5 +``` + +Example response: + +```json +{ + "id": 30, + "iid": 5, + "group_id": 7, + "title": "Ea cupiditate dolores ut vero consequatur quasi veniam voluptatem et non.", + "description": "Molestias dolorem eos vitae expedita impedit necessitatibus quo voluptatum.", + "state": "opened", + "author":{ + "id": 7, + "name": "Pamella Huel", + "username": "arnita", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/a2f5c6fcef64c9c69cb8779cb292be1b?s=80&d=identicon", + "web_url": "http://localhost:3001/arnita" + }, + "start_date": null, + "start_date_is_fixed": false, + "start_date_fixed": null, + "start_date_from_milestones": null, + "end_date": "2018-07-31", + "due_date": "2018-07-31", + "due_date_is_fixed": false, + "due_date_fixed": null, + "due_date_from_milestones": "2018-07-31", + "created_at": "2018-07-17T13:36:22.770Z", + "updated_at": "2018-07-18T12:22:05.239Z", + "labels": [], + "upvotes": 4, + "downvotes": 0 +} +``` + +## New epic + +Creates a new epic. + +NOTE: **Note:** +Starting with GitLab [11.3][ee-6448], `start_date` and `end_date` should no longer be assigned +directly, as they now represent composite values. You can configure it via the `*_is_fixed` and +`*_fixed` fields instead. + +``` +POST /groups/:id/epics +``` + +| Attribute | Type | Required | Description | +| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | +| `title` | string | yes | The title of the epic | +| `labels` | string | no | The comma separated list of labels | +| `description` | string | no | The description of the epic | +| `start_date_is_fixed` | boolean | no | Whether start date should be sourced from `start_date_fixed` or from milestones (since 11.3) | +| `start_date_fixed` | string | no | The fixed start date of an epic (since 11.3) | +| `due_date_is_fixed` | boolean | no | Whether due date should be sourced from `due_date_fixed` or from milestones (since 11.3) | +| `due_date_fixed` | string | no | The fixed due date of an epic (since 11.3) | +| `parent_id` | integer/string | no | The id of a parent epic (since 11.11) | + +```bash +curl --header POST "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics?title=Epic&description=Epic%20description +``` + +Example response: + +```json +{ + "id": 33, + "iid": 6, + "group_id": 7, + "title": "Epic", + "description": "Epic description", + "state": "opened", + "author": { + "name" : "Alexandra Bashirian", + "avatar_url" : null, + "state" : "active", + "web_url" : "https://gitlab.example.com/eileen.lowe", + "id" : 18, + "username" : "eileen.lowe" + }, + "start_date": null, + "start_date_is_fixed": false, + "start_date_fixed": null, + "start_date_from_milestones": null, + "end_date": "2018-07-31", + "due_date": "2018-07-31", + "due_date_is_fixed": false, + "due_date_fixed": null, + "due_date_from_milestones": "2018-07-31", + "created_at": "2018-07-17T13:36:22.770Z", + "updated_at": "2018-07-18T12:22:05.239Z", + "labels": [], + "upvotes": 4, + "downvotes": 0 +} +``` + +## Update epic + +Updates an epic. + +NOTE: **Note:** +Starting with GitLab [11.3][ee-6448], `start_date` and `end_date` should no longer be assigned +directly, as they now represent composite values. You can configure it via the `*_is_fixed` and +`*_fixed` fields instead. + +``` +PUT /groups/:id/epics/:epic_iid +``` + +| Attribute | Type | Required | Description | +| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | +| `epic_iid` | integer/string | yes | The internal ID of the epic | +| `title` | string | no | The title of an epic | +| `description` | string | no | The description of an epic | +| `labels` | string | no | The comma separated list of labels | +| `start_date_is_fixed` | boolean | no | Whether start date should be sourced from `start_date_fixed` or from milestones (since 11.3) | +| `start_date_fixed` | string | no | The fixed start date of an epic (since 11.3) | +| `due_date_is_fixed` | boolean | no | Whether due date should be sourced from `due_date_fixed` or from milestones (since 11.3) | +| `due_date_fixed` | string | no | The fixed due date of an epic (since 11.3) | +| `state_event` | string | no | State event for an epic. Set `close` to close the epic and `reopen` to reopen it (since 11.4) | + +```bash +curl --header PUT "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/5?title=New%20Title +``` + +Example response: + +```json +{ + "id": 33, + "iid": 6, + "group_id": 7, + "title": "New Title", + "description": "Epic description", + "state": "opened", + "author": { + "name" : "Alexandra Bashirian", + "avatar_url" : null, + "state" : "active", + "web_url" : "https://gitlab.example.com/eileen.lowe", + "id" : 18, + "username" : "eileen.lowe" + }, + "start_date": null, + "start_date_is_fixed": false, + "start_date_fixed": null, + "start_date_from_milestones": null, + "end_date": "2018-07-31", + "due_date": "2018-07-31", + "due_date_is_fixed": false, + "due_date_fixed": null, + "due_date_from_milestones": "2018-07-31", + "created_at": "2018-07-17T13:36:22.770Z", + "updated_at": "2018-07-18T12:22:05.239Z", + "labels": [], + "upvotes": 4, + "downvotes": 0 +} +``` + +## Delete epic + +Deletes an epic + +``` +DELETE /groups/:id/epics/:epic_iid +``` + +| Attribute | Type | Required | Description | +| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | +| `epic_iid` | integer/string | yes | The internal ID of the epic. | + +```bash +curl --header DELETE "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/5 +``` + +## Create a todo + +Manually creates a todo for the current user on an epic. If +there already exists a todo for the user on that epic, status code `304` is +returned. + +``` +POST /groups/:id/epics/:epic_iid/todo +``` + +| Attribute | Type | Required | Description | +|-------------|---------|----------|--------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | +| `epic_iid ` | integer | yes | The internal ID of a group's epic | + +```bash +curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/5/todo +``` + +Example response: + +```json +{ + "id": 112, + "group": { + "id": 1, + "name": "Gitlab", + "path": "gitlab", + "kind": "group", + "full_path": "base/gitlab", + "parent_id": null + }, + "author": { + "name": "Administrator", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "https://gitlab.example.com/root" + }, + "action_name": "marked", + "target_type": "epic", + "target": { + "id": 30, + "iid": 5, + "group_id": 1, + "title": "Ea cupiditate dolores ut vero consequatur quasi veniam voluptatem et non.", + "description": "Molestias dolorem eos vitae expedita impedit necessitatibus quo voluptatum.", + "author":{ + "id": 7, + "name": "Pamella Huel", + "username": "arnita", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/a2f5c6fcef64c9c69cb8779cb292be1b?s=80&d=identicon", + "web_url": "http://localhost:3001/arnita" + }, + "start_date": null, + "end_date": null, + "created_at": "2018-01-21T06:21:13.165Z", + "updated_at": "2018-01-22T12:41:41.166Z" + }, + "target_url": "https://gitlab.example.com/groups/epics/5", + "body": "Vel voluptas atque dicta mollitia adipisci qui at.", + "state": "pending", + "created_at": "2016-07-01T11:09:13.992Z" +} +``` + +[ee-6448]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6448 diff --git a/doc/api/geo_nodes.md b/doc/api/geo_nodes.md new file mode 100644 index 00000000000..a1cb524499f --- /dev/null +++ b/doc/api/geo_nodes.md @@ -0,0 +1,408 @@ +# Geo Nodes API **[PREMIUM ONLY]** + +In order to interact with Geo node endpoints, you need to authenticate yourself +as an admin. + +## Retrieve configuration about all Geo nodes + +``` +GET /geo_nodes +``` + +```bash +curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes +``` + +Example response: + +```json +[ + { + "id": 1, + "name": "us-node", + "url": "https://primary.example.com/", + "internal_url": "https://internal.example.com/", + "primary": true, + "enabled": true, + "current": true, + "files_max_capacity": 10, + "repos_max_capacity": 25, + "verification_max_capacity": 100, + "clone_protocol": "http" + }, + { + "id": 2, + "name": "cn-node", + "url": "https://secondary.example.com/", + "internal_url": "https://secondary.example.com/", + "primary": false, + "enabled": true, + "current": false, + "files_max_capacity": 10, + "repos_max_capacity": 25, + "verification_max_capacity": 100, + "clone_protocol": "http" + } +] +``` + +## Retrieve configuration about a specific Geo node + +``` +GET /geo_nodes/:id +``` + +```bash +curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes/1 +``` + +Example response: + +```json +{ + "id": 1, + "name": "us-node", + "url": "https://primary.example.com/", + "internal_url": "https://primary.example.com/", + "primary": true, + "enabled": true, + "current": true, + "files_max_capacity": 10, + "repos_max_capacity": 25, + "verification_max_capacity": 100, + "clone_protocol": "http" +} +``` + +## Edit a Geo node + +Updates settings of an existing Geo node. + +_This can only be run against a primary Geo node._ + +``` +PUT /geo_nodes/:id +``` + +| Attribute | Type | Required | Description | +|----------------------|---------|-----------|---------------------------------------------------------------------------| +| `id` | integer | yes | The ID of the Geo node. | +| `enabled` | boolean | no | Flag indicating if the Geo node is enabled. | +| `name` | string | yes | The unique identifier for the Geo node. Must match `geo_node_name` if it is set in gitlab.rb, otherwise it must match `external_url`. | +| `url` | string | yes | The user-facing URL of the Geo node. | +| `internal_url` | string | no | The URL defined on the primary node that secondary nodes should use to contact it. Returns `url` if not set.| +| `files_max_capacity` | integer | no | Control the maximum concurrency of LFS/attachment backfill for this secondary node. | +| `repos_max_capacity` | integer | no | Control the maximum concurrency of repository backfill for this secondary node. | +| `verification_max_capacity` | integer | no | Control the maximum concurrency of verification for this node. | + +Example response: + +```json +{ + "id": 1, + "name": "cn-node", + "url": "https://secondary.example.com/", + "internal_url": "https://secondary.example.com/", + "primary": false, + "enabled": true, + "current": true, + "files_max_capacity": 10, + "repos_max_capacity": 25, + "verification_max_capacity": 100, + "clone_protocol": "http" +} +``` + +## Delete a Geo node + +Removes the Geo node. + +NOTE: **Note:** +Only a Geo primary node will accept this request. + +``` +DELETE /geo_nodes/:id +``` + +| Attribute | Type | Required | Description | +|-----------|---------|----------|-------------------------| +| `id` | integer | yes | The ID of the Geo node. | + +## Repair a Geo node + +To repair the OAuth authentication of a Geo node. + +_This can only be run against a primary Geo node._ + +``` +POST /geo_nodes/:id/repair +``` + +Example response: + +```json +{ + "id": 1, + "name": "us-node", + "url": "https://primary.example.com/", + "internal_url": "https://primary.example.com/", + "primary": true, + "enabled": true, + "current": true, + "files_max_capacity": 10, + "repos_max_capacity": 25, + "verification_max_capacity": 100, + "clone_protocol": "http" +} +``` + +## Retrieve status about all Geo nodes + +``` +GET /geo_nodes/status +``` + +```bash +curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes/status +``` + +Example response: + +```json +[ + { + "geo_node_id": 1, + "healthy": true, + "health": "Healthy", + "health_status": "Healthy", + "missing_oauth_application": false, + "attachments_count": 1, + "attachments_synced_count": nil, + "attachments_failed_count": nil, + "attachments_synced_missing_on_primary_count": 0, + "attachments_synced_in_percentage": "0.00%", + "db_replication_lag_seconds": nil, + "lfs_objects_count": 0, + "lfs_objects_synced_count": nil, + "lfs_objects_failed_count": nil, + "lfs_objects_synced_missing_on_primary_count": 0, + "lfs_objects_synced_in_percentage": "0.00%", + "job_artifacts_count": 2, + "job_artifacts_synced_count": nil, + "job_artifacts_failed_count": nil, + "job_artifacts_synced_missing_on_primary_count": 0, + "job_artifacts_synced_in_percentage": "0.00%", + "repositories_count": 41, + "projects_count": 41, + "repositories_failed_count": nil, + "repositories_synced_count": nil, + "repositories_synced_in_percentage": "0.00%", + "wikis_count": 41, + "wikis_failed_count": nil, + "wikis_synced_count": nil, + "wikis_synced_in_percentage": "0.00%", + "replication_slots_count": 1, + "replication_slots_used_count": 1, + "replication_slots_used_in_percentage": "100.00%", + "replication_slots_max_retained_wal_bytes": 0, + "repositories_checked_count": 20, + "repositories_checked_failed_count": 20, + "repositories_checked_in_percentage": "100.00%", + "repositories_checksummed_count": 20, + "repositories_checksum_failed_count": 5, + "repositories_checksummed_in_percentage": "48.78%", + "wikis_checksummed_count": 10, + "wikis_checksum_failed_count": 3, + "wikis_checksummed_in_percentage": "24.39%", + "repositories_verified_count": 20, + "repositories_verification_failed_count": 5, + "repositories_verified_in_percentage": "48.78%", + "repositories_checksum_mismatch_count": 3, + "wikis_verified_count": 10, + "wikis_verification_failed_count": 3, + "wikis_verified_in_percentage": "24.39%", + "wikis_checksum_mismatch_count": 1, + "repositories_retrying_verification_count": 1, + "wikis_retrying_verification_count": 3, + "repositories_checked_count": 7, + "repositories_checked_failed_count": 2, + "repositories_checked_in_percentage": "17.07%", + "last_event_id": 23, + "last_event_timestamp": 1509681166, + "cursor_last_event_id": nil, + "cursor_last_event_timestamp": 0, + "last_successful_status_check_timestamp": 1510125024, + "version": "10.3.0", + "revision": "33d33a096a", + }, + { + "geo_node_id": 2, + "healthy": true, + "health": "Healthy", + "health_status": "Healthy", + "missing_oauth_application": false, + "attachments_count": 1, + "attachments_synced_count": 1, + "attachments_failed_count": 0, + "attachments_synced_missing_on_primary_count": 0, + "attachments_synced_in_percentage": "100.00%", + "db_replication_lag_seconds": 0, + "lfs_objects_count": 0, + "lfs_objects_synced_count": 0, + "lfs_objects_failed_count": 0, + "lfs_objects_synced_missing_on_primary_count": 0, + "lfs_objects_synced_in_percentage": "0.00%", + "job_artifacts_count": 2, + "job_artifacts_synced_count": 1, + "job_artifacts_failed_count": 1, + "job_artifacts_synced_missing_on_primary_count": 0, + "job_artifacts_synced_in_percentage": "50.00%", + "repositories_count": 41, + "projects_count": 41, + "repositories_failed_count": 1, + "repositories_synced_count": 40, + "repositories_synced_in_percentage": "97.56%", + "wikis_count": 41, + "wikis_failed_count": 0, + "wikis_synced_count": 41, + "wikis_synced_in_percentage": "100.00%", + "replication_slots_count": nil, + "replication_slots_used_count": nil, + "replication_slots_used_in_percentage": "0.00%", + "replication_slots_max_retained_wal_bytes": nil, + "repositories_checksummed_count": 20, + "repositories_checksum_failed_count": 5, + "repositories_checksummed_in_percentage": "48.78%", + "wikis_checksummed_count": 10, + "wikis_checksum_failed_count": 3, + "wikis_checksummed_in_percentage": "24.39%", + "repositories_verified_count": 20, + "repositories_verification_failed_count": 5, + "repositories_verified_in_percentage": "48.78%", + "repositories_checksum_mismatch_count": 3, + "wikis_verified_count": 10, + "wikis_verification_failed_count": 3, + "wikis_verified_in_percentage": "24.39%", + "wikis_checksum_mismatch_count": 1, + "repositories_retrying_verification_count": 4, + "wikis_retrying_verification_count": 2, + "repositories_checked_count": 5, + "repositories_checked_failed_count": 1, + "repositories_checked_in_percentage": "12.20%", + "last_event_id": 23, + "last_event_timestamp": 1509681166, + "cursor_last_event_id": 23, + "cursor_last_event_timestamp": 1509681166, + "last_successful_status_check_timestamp": 1510125024, + "version": "10.3.0", + "revision": "33d33a096a" + } +] +``` + +Note: fields `wikis_count` and `repositories_count` are deprecated and will be deleted soon. Please use `projects_count` instead. + +## Retrieve status about a specific Geo node + +``` +GET /geo_nodes/:id/status +``` + +```bash +curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes/2/status +``` + +Example response: + +```json +{ + "geo_node_id": 2, + "healthy": true, + "health": "Healthy", + "health_status": "Healthy", + "missing_oauth_application": false, + "attachments_count": 1, + "attachments_synced_count": 1, + "attachments_failed_count": 0, + "attachments_synced_missing_on_primary_count": 0, + "attachments_synced_in_percentage": "100.00%", + "db_replication_lag_seconds": 0, + "lfs_objects_count": 0, + "lfs_objects_synced_count": 0, + "lfs_objects_failed_count": 0, + "lfs_objects_synced_missing_on_primary_count": 0, + "lfs_objects_synced_in_percentage": "0.00%", + "job_artifacts_count": 2, + "job_artifacts_synced_count": 1, + "job_artifacts_failed_count": 1, + "job_artifacts_synced_missing_on_primary_count": 0, + "job_artifacts_synced_in_percentage": "50.00%", + "repositories_count": 41, + "projects_count": 41, + "repositories_failed_count": 1, + "repositories_synced_count": 40, + "repositories_synced_in_percentage": "97.56%", + "wikis_count": 41, + "wikis_failed_count": 0, + "wikis_synced_count": 41, + "wikis_synced_in_percentage": "100.00%", + "replication_slots_count": nil, + "replication_slots_used_count": nil, + "replication_slots_used_in_percentage": "0.00%", + "replication_slots_max_retained_wal_bytes": nil, + "last_event_id": 23, + "last_event_timestamp": 1509681166, + "cursor_last_event_id": 23, + "cursor_last_event_timestamp": 1509681166, + "last_successful_status_check_timestamp": 1510125268, + "version": "10.3.0", + "revision": "33d33a096a" +} +``` + +Note: The `health_status` parameter can only be in an "Healthy" or "Unhealthy" state, while the `health` parameter can be empty, "Healthy", or contain the actual error message. + +Note: Fields `wikis_count` and `repositories_count` are deprecated and will be deleted soon. Please use `projects_count` instead. + +## Retrieve project sync or verification failures that occurred on the current node + +This only works on a secondary node. + +``` +GET /geo_nodes/current/failures +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `type` | string | no | Type of failed objects (`repository`/`wiki`) | +| `failure_type` | string | no | Type of failures (`sync`/`checksum_mismatch`/`verification`) | + +This endpoint uses [Pagination](README.md#pagination). + +```bash +curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes/current/failures +``` + +Example response: + +```json +[ + { + "project_id": 3, + "last_repository_synced_at": "2017-10-31 14:25:55 UTC", + "last_repository_successful_sync_at": "2017-10-31 14:26:04 UTC", + "last_wiki_synced_at": "2017-10-31 14:26:04 UTC", + "last_wiki_successful_sync_at": "2017-10-31 14:26:11 UTC", + "repository_retry_count": null, + "wiki_retry_count": 1, + "last_repository_sync_failure": null, + "last_wiki_sync_failure": "Error syncing Wiki repository", + "last_repository_verification_failure": "", + "last_wiki_verification_failure": "", + "repository_verification_checksum_sha": "da39a3ee5e6b4b0d32e5bfef9a601890afd80709", + "wiki_verification_checksum_sha": "da39a3ee5e6b4b0d3255bfef9ef0189aafd80709", + "repository_checksum_mismatch": false, + "wiki_checksum_mismatch": false + } +] +``` diff --git a/doc/api/issue_links.md b/doc/api/issue_links.md new file mode 100644 index 00000000000..1c7db6a8e4c --- /dev/null +++ b/doc/api/issue_links.md @@ -0,0 +1,215 @@ +# Issue links API **[STARTER]** + +## List issue relations + +Get a list of related issues of a given issue, sorted by the relationship creation datetime (ascending). +Issues will be filtered according to the user authorizations. + +``` +GET /projects/:id/issues/:issue_iid/links +``` + +Parameters: + +| Attribute | Type | Required | Description | +|-------------|---------|----------|--------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `issue_iid` | integer | yes | The internal ID of a project's issue | + +```json +[ + { + "id" : 84, + "iid" : 14, + "issue_link_id": 1 + "project_id" : 4, + "created_at" : "2016-01-07T12:44:33.959Z", + "title" : "Issues with auth", + "state" : "opened", + "assignees" : [], + "assignee" : null, + "labels" : [ + "bug" + ], + "author" : { + "name" : "Alexandra Bashirian", + "avatar_url" : null, + "state" : "active", + "web_url" : "https://gitlab.example.com/eileen.lowe", + "id" : 18, + "username" : "eileen.lowe" + }, + "description" : null, + "updated_at" : "2016-01-07T12:44:33.959Z", + "milestone" : null, + "subscribed" : true, + "user_notes_count": 0, + "due_date": null, + "web_url": "http://example.com/example/example/issues/14", + "confidential": false, + "weight": null, + } +] +``` + +## Create an issue link + +Creates a two-way relation between two issues. User must be allowed to update both issues in order to succeed. + +``` +POST /projects/:id/issues/:issue_iid/links +``` + +| Attribute | Type | Required | Description | +|-------------|---------|----------|--------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `issue_iid` | integer | yes | The internal ID of a project's issue | +| `target_project_id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) of a target project | +| `target_issue_iid` | integer/string | yes | The internal ID of a target project's issue | + + +```json +{ + "source_issue" : { + "id" : 83, + "iid" : 11, + "project_id" : 4, + "created_at" : "2016-01-07T12:44:33.959Z", + "title" : "Issues with auth", + "state" : "opened", + "assignees" : [], + "assignee" : null, + "labels" : [ + "bug" + ], + "author" : { + "name" : "Alexandra Bashirian", + "avatar_url" : null, + "state" : "active", + "web_url" : "https://gitlab.example.com/eileen.lowe", + "id" : 18, + "username" : "eileen.lowe" + }, + "description" : null, + "updated_at" : "2016-01-07T12:44:33.959Z", + "milestone" : null, + "subscribed" : true, + "user_notes_count": 0, + "due_date": null, + "web_url": "http://example.com/example/example/issues/11", + "confidential": false, + "weight": null, + }, + "target_issue" : { + "id" : 84, + "iid" : 14, + "project_id" : 4, + "created_at" : "2016-01-07T12:44:33.959Z", + "title" : "Issues with auth", + "state" : "opened", + "assignees" : [], + "assignee" : null, + "labels" : [ + "bug" + ], + "author" : { + "name" : "Alexandra Bashirian", + "avatar_url" : null, + "state" : "active", + "web_url" : "https://gitlab.example.com/eileen.lowe", + "id" : 18, + "username" : "eileen.lowe" + }, + "description" : null, + "updated_at" : "2016-01-07T12:44:33.959Z", + "milestone" : null, + "subscribed" : true, + "user_notes_count": 0, + "due_date": null, + "web_url": "http://example.com/example/example/issues/14", + "confidential": false, + "weight": null, + } +} +``` + +## Delete an issue link + +Deletes an issue link, thus removes the two-way relationship. + +``` +DELETE /projects/:id/issues/:issue_iid/links/:issue_link_id +``` + + +| Attribute | Type | Required | Description | +|-------------|---------|----------|--------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `issue_iid` | integer | yes | The internal ID of a project's issue | +| `issue_link_id` | integer/string | yes | The ID of an issue relationship | + + +```json +{ + "source_issue" : { + "id" : 83, + "iid" : 11, + "project_id" : 4, + "created_at" : "2016-01-07T12:44:33.959Z", + "title" : "Issues with auth", + "state" : "opened", + "assignees" : [], + "assignee" : null, + "labels" : [ + "bug" + ], + "author" : { + "name" : "Alexandra Bashirian", + "avatar_url" : null, + "state" : "active", + "web_url" : "https://gitlab.example.com/eileen.lowe", + "id" : 18, + "username" : "eileen.lowe" + }, + "description" : null, + "updated_at" : "2016-01-07T12:44:33.959Z", + "milestone" : null, + "subscribed" : true, + "user_notes_count": 0, + "due_date": null, + "web_url": "http://example.com/example/example/issues/11", + "confidential": false, + "weight": null, + }, + "target_issue" : { + "id" : 84, + "iid" : 14, + "project_id" : 4, + "created_at" : "2016-01-07T12:44:33.959Z", + "title" : "Issues with auth", + "state" : "opened", + "assignees" : [], + "assignee" : null, + "labels" : [ + "bug" + ], + "author" : { + "name" : "Alexandra Bashirian", + "avatar_url" : null, + "state" : "active", + "web_url" : "https://gitlab.example.com/eileen.lowe", + "id" : 18, + "username" : "eileen.lowe" + }, + "description" : null, + "updated_at" : "2016-01-07T12:44:33.959Z", + "milestone" : null, + "subscribed" : true, + "user_notes_count": 0, + "due_date": null, + "web_url": "http://example.com/example/example/issues/14", + "confidential": false, + "weight": null, + } +} +``` diff --git a/doc/api/issues.md b/doc/api/issues.md index cb5789e76b7..4fb3626f637 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -37,23 +37,26 @@ GET /issues?confidential=true | Attribute | Type | Required | Description | | ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -| `state` | string | no | Return all issues or just those that are `opened` or `closed` | +| `state` | string | no | Return `all` issues or just those that are `opened` or `closed` | | `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. `No+Label` (Deprecated) lists all issues with no labels. Predefined names are case-insensitive. | +| `with_labels_details`| Boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:text_color`. Default is `false`. | | `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. | | `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | -| `author_id` | integer | no | Return issues created by the given user `id`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ | -| `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ | +| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ | +| `author_username` | string | no | Return issues created by the given `username`. Simillar to `author_id` and mutually exclusive with `author_id`. | +| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ | +| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Simillar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. | | `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ | | `iids[]` | Array[integer] | no | Return only the issues having the given `iid` | | `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` | | `search` | string | no | Search issues against their `title` and `description` | -| `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joining them with comma. Default is `title,description` | +| `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joining them with comma. Default is `title,description` | | `created_after` | datetime | no | Return issues created on or after the given time | | `created_before` | datetime | no | Return issues created on or before the given time | | `updated_after` | datetime | no | Return issues updated on or after the given time | | `updated_before` | datetime | no | Return issues updated on or before the given time | -| `confidential ` | Boolean | no | Filter confidential or public issues. | +| `confidential ` | Boolean | no | Filter confidential or public issues. | ```bash curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/issues @@ -109,7 +112,7 @@ Example response: "title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.", "created_at" : "2016-01-04T15:31:51.081Z", "iid" : 6, - "labels" : [], + "labels" : ["foo", "bar"], "upvotes": 4, "downvotes": 0, "merge_requests_count": 0, @@ -122,8 +125,17 @@ Example response: "human_time_estimate": null, "human_total_time_spent": null }, + "has_tasks": true, + "task_status": "10 of 15 tasks completed", "confidential": false, - "discussion_locked": false + "discussion_locked": false, + "_links":{ + "self":"http://example.com/api/v4/projects/1/issues/76", + "notes":"`http://example.com/`api/v4/projects/1/issues/76/notes", + "award_emoji":"http://example.com/api/v4/projects/1/issues/76/award_emoji", + "project":"http://example.com/api/v4/projects/1" + }, + "subscribed": false } ] ``` @@ -158,11 +170,14 @@ GET /groups/:id/issues?confidential=true | `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | | `state` | string | no | Return all issues or just those that are `opened` or `closed` | | `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. `No+Label` (Deprecated) lists all issues with no labels. Predefined names are case-insensitive. | +| `with_labels_details`| Boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:text_color`. Default is `false`. | | `iids[]` | Array[integer] | no | Return only the issues having the given `iid` | | `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. | | `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | -| `author_id` | integer | no | Return issues created by the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ | -| `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ | +| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ | +| `author_username` | string | no | Return issues created by the given `username`. Simillar to `author_id` and mutually exclusive with `author_id`. | +| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ | +| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Simillar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. | | `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ | | `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` | @@ -221,7 +236,7 @@ Example response: "id" : 9, "name" : "Dr. Luella Kovacek" }, - "labels" : [], + "labels" : ["foo", "bar"], "upvotes": 4, "downvotes": 0, "merge_requests_count": 0, @@ -240,8 +255,17 @@ Example response: "human_time_estimate": null, "human_total_time_spent": null }, + "has_tasks": true, + "task_status": "10 of 15 tasks completed", "confidential": false, - "discussion_locked": false + "discussion_locked": false, + "_links":{ + "self":"http://example.com/api/v4/projects/4/issues/41", + "notes":"`http://example.com/`api/v4/projects/4/issues/41/notes", + "award_emoji":"http://example.com/api/v4/projects/4/issues/41/award_emoji", + "project":"http://example.com/api/v4/projects/4" + }, + "subscribed": false } ] ``` @@ -277,10 +301,13 @@ GET /projects/:id/issues?confidential=true | `iids[]` | Array[integer] | no | Return only the milestone having the given `iid` | | `state` | string | no | Return all issues or just those that are `opened` or `closed` | | `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. `No+Label` (Deprecated) lists all issues with no labels. Predefined names are case-insensitive. | +| `with_labels_details`| Boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:text_color`. Default is `false`. | | `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. | | `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | -| `author_id` | integer | no | Return issues created by the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ | -| `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ | +| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ | +| `author_username` | string | no | Return issues created by the given `username`. Simillar to `author_id` and mutually exclusive with `author_id`. | +| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ | +| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Simillar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. | | `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ | | `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` | @@ -340,7 +367,7 @@ Example response: "id" : 9, "name" : "Dr. Luella Kovacek" }, - "labels" : [], + "labels" : ["foo", "bar"], "upvotes": 4, "downvotes": 0, "merge_requests_count": 0, @@ -366,8 +393,17 @@ Example response: "human_time_estimate": null, "human_total_time_spent": null }, + "has_tasks": true, + "task_status": "10 of 15 tasks completed", "confidential": false, - "discussion_locked": false + "discussion_locked": false, + "_links":{ + "self":"http://example.com/api/v4/projects/4/issues/41", + "notes":"`http://example.com/`api/v4/projects/4/issues/41/notes", + "award_emoji":"http://example.com/api/v4/projects/4/issues/41/award_emoji", + "project":"http://example.com/api/v4/projects/4" + }, + "subscribed": false } ] ``` diff --git a/doc/api/issues_statistics.md b/doc/api/issues_statistics.md new file mode 100644 index 00000000000..82bc9c142cc --- /dev/null +++ b/doc/api/issues_statistics.md @@ -0,0 +1,177 @@ +# Issues Statistics API + +Every API call to issues_statistics must be authenticated. + +If a user is not a member of a project and the project is private, a `GET` +request on that project will result to a `404` status code. + +## Get issues statistics + +Gets issues count statistics on all issues the authenticated user has access to. By default it +returns only issues created by the current user. To get all issues, +use parameter `scope=all`. + +``` +GET /issues_statistics +GET /issues_statistics?labels=foo +GET /issues_statistics?labels=foo,bar +GET /issues_statistics?labels=foo,bar&state=opened +GET /issues_statistics?milestone=1.0.0 +GET /issues_statistics?milestone=1.0.0&state=opened +GET /issues_statistics?iids[]=42&iids[]=43 +GET /issues_statistics?author_id=5 +GET /issues_statistics?assignee_id=5 +GET /issues_statistics?my_reaction_emoji=star +GET /issues_statistics?search=foo&in=title +GET /issues_statistics?confidential=true +``` + +| Attribute | Type | Required | Description | +| ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. | +| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. | +| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me` | +| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. | +| `author_username` | string | no | Return issues created by the given `username`. Similar to `author_id` and mutually exclusive with `author_id`. | +| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. | +| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. | +| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. | +| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` | +| `search` | string | no | Search issues against their `title` and `description` | +| `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joining them with comma. Default is `title,description` | +| `created_after` | datetime | no | Return issues created on or after the given time | +| `created_before` | datetime | no | Return issues created on or before the given time | +| `updated_after` | datetime | no | Return issues updated on or after the given time | +| `updated_before` | datetime | no | Return issues updated on or before the given time | +| `confidential ` | Boolean | no | Filter confidential or public issues. | + +```bash +curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/issues_statistics +``` + +Example response: + +```json +{ + "statistics": { + "counts": { + "all": 20, + "closed": 5, + "opened": 15 + } + } +} +``` + +## Get group issues statistics + +Gets issues count statistics for given group. + +``` +GET /groups/:id/issues_statistics +GET /groups/:id/issues_statistics?labels=foo +GET /groups/:id/issues_statistics?labels=foo,bar +GET /groups/:id/issues_statistics?labels=foo,bar&state=opened +GET /groups/:id/issues_statistics?milestone=1.0.0 +GET /groups/:id/issues_statistics?milestone=1.0.0&state=opened +GET /groups/:id/issues_statistics?iids[]=42&iids[]=43 +GET /groups/:id/issues_statistics?search=issue+title+or+description +GET /groups/:id/issues_statistics?author_id=5 +GET /groups/:id/issues_statistics?assignee_id=5 +GET /groups/:id/issues_statistics?my_reaction_emoji=star +GET /groups/:id/issues_statistics?confidential=true +``` + +| Attribute | Type | Required | Description | +| ------------------- | ---------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | +| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. | +| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` | +| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. | +| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. | +| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. | +| `author_username` | string | no | Return issues created by the given `username`. Similar to `author_id` and mutually exclusive with `author_id`. | +| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. | +| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. | +| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. | +| `search` | string | no | Search group issues against their `title` and `description` | +| `created_after` | datetime | no | Return issues created on or after the given time | +| `created_before` | datetime | no | Return issues created on or before the given time | +| `updated_after` | datetime | no | Return issues updated on or after the given time | +| `updated_before` | datetime | no | Return issues updated on or before the given time | +| `confidential ` | Boolean | no | Filter confidential or public issues. | + +```bash +curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/4/issues_statistics +``` + +Example response: + +```json +{ + "statistics": { + "counts": { + "all": 20, + "closed": 5, + "opened": 15 + } + } +} +``` + +## Get project issues statistics + +Gets issues count statistics for given project. + +``` +GET /projects/:id/issues_statistics +GET /projects/:id/issues_statistics?labels=foo +GET /projects/:id/issues_statistics?labels=foo,bar +GET /projects/:id/issues_statistics?labels=foo,bar&state=opened +GET /projects/:id/issues_statistics?milestone=1.0.0 +GET /projects/:id/issues_statistics?milestone=1.0.0&state=opened +GET /projects/:id/issues_statistics?iids[]=42&iids[]=43 +GET /projects/:id/issues_statistics?search=issue+title+or+description +GET /projects/:id/issues_statistics?author_id=5 +GET /projects/:id/issues_statistics?assignee_id=5 +GET /projects/:id/issues_statistics?my_reaction_emoji=star +GET /projects/:id/issues_statistics?confidential=true +``` + +| Attribute | Type | Required | Description | +| ------------------- | ---------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `iids[]` | Array[integer] | no | Return only the milestone having the given `iid` | +| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. | +| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. | +| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. | +| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. | +| `author_username` | string | no | Return issues created by the given `username`. Similar to `author_id` and mutually exclusive with `author_id`. | +| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. | +| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. | +| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. | +| `search` | string | no | Search project issues against their `title` and `description` | +| `created_after` | datetime | no | Return issues created on or after the given time | +| `created_before` | datetime | no | Return issues created on or before the given time | +| `updated_after` | datetime | no | Return issues updated on or after the given time | +| `updated_before` | datetime | no | Return issues updated on or before the given time | +| `confidential ` | Boolean | no | Filter confidential or public issues. | + + +```bash +curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/4/issues_statistics +``` + +Example response: + +```json +{ + "statistics": { + "counts": { + "all": 20, + "closed": 5, + "opened": 15 + } + } +} +``` diff --git a/doc/api/license.md b/doc/api/license.md new file mode 100644 index 00000000000..2a8de64bdbf --- /dev/null +++ b/doc/api/license.md @@ -0,0 +1,176 @@ +# License **[CORE ONLY]** + +In order to interact with license endpoints, you need to authenticate yourself +as an admin. + +## Retrieve information about the current license + +``` +GET /license +``` + +```json +{ + "id": 2, + "plan": "gold", + "created_at": "2018-02-27T23:21:58.674Z", + "starts_at": "2018-01-27", + "expires_at": "2022-01-27", + "historical_max": 300, + "expired": false, + "overage": 200, + "user_limit": 100, + "active_users": 300, + "licensee": { + "Name": "John Doe1" + }, + "add_ons": { + "GitLab_FileLocks": 1, + "GitLab_Auditor_User": 1 + } +} +``` + +## Retrieve information about all licenses + +``` +GET /licenses +``` + +```json +[ + { + "id": 1, + "plan": "silver", + "created_at": "2018-02-27T23:21:58.674Z", + "starts_at": "2018-01-27", + "expires_at": "2022-01-27", + "historical_max": 300, + "expired": false, + "overage": 200, + "user_limit": 100, + "licensee": { + "Name": "John Doe1" + }, + "add_ons": { + "GitLab_FileLocks": 1, + "GitLab_Auditor_User": 1 + } + }, + { + "id": 2, + "plan": "gold", + "created_at": "2018-02-27T23:21:58.674Z", + "starts_at": "2018-01-27", + "expires_at": "2022-01-27", + "historical_max": 300, + "expired": false, + "overage": 200, + "user_limit": 100, + "licensee": { + "Name": "Doe John" + }, + "add_ons": { + "GitLab_FileLocks": 1, + } + } +] +``` + +Overage is the difference between the number of active users and the licensed number of users. +This is calculated differently depending on whether the license has expired or not. + +- If the license has expired, it uses the historical maximum active user count (`historical_max`). +- If the license has not expired, it uses the current active users count. + +Returns: + +- `200 OK` with response containing the licenses in JSON format. This will be an empty JSON array if there are no licenses. +- `403 Forbidden` if the current user in not permitted to read the licenses. + +## Add a new license + +``` +POST /license +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `license` | string | yes | The license string | + +```bash +curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/license?license=eyJkYXRhIjoiMHM5Q...S01Udz09XG4ifQ==" +``` + +Example response: + +```json +{ + "id": 1, + "plan": "gold", + "created_at": "2018-02-27T23:21:58.674Z", + "starts_at": "2018-01-27", + "expires_at": "2022-01-27", + "historical_max": 300, + "expired": false, + "overage": 200, + "user_limit": 100, + "active_users": 300, + "licensee": { + "Name": "John Doe1" + }, + "add_ons": { + "GitLab_FileLocks": 1, + "GitLab_Auditor_User": 1 + } +} +``` + +Returns: + +- `201 Created` if the license is successfully added. +- `400 Bad Request` if the license couldn't be added, with an error message explaining the reason. + + +## Delete a license + +``` +DELETE /license/:id +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | ID of the GitLab license. | + +```bash +curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/license/:id" +``` + +Example response: + +```json +{ + "id": 2, + "plan": "gold", + "created_at": "2018-02-27T23:21:58.674Z", + "starts_at": "2018-01-27", + "expires_at": "2022-01-27", + "historical_max": 300, + "expired": false, + "overage": 200, + "user_limit": 100, + "licensee": { + "Name": "John Doe" + }, + "add_ons": { + "GitLab_FileLocks": 1, + "GitLab_Auditor_User": 1 + } +} +``` + +Returns: + +- `204 No Content` if the license is successfully deleted. +- `403 Forbidden` if the current user in not permitted to delete the license. +- `404 Not Found` if the license to delete could not be found. diff --git a/doc/api/license_templates.md b/doc/api/license_templates.md new file mode 100644 index 00000000000..1b68af9ce31 --- /dev/null +++ b/doc/api/license_templates.md @@ -0,0 +1,5 @@ +--- +redirect_to: 'templates/licenses.md' +--- + +This document was moved to [another location](templates/licenses.md). diff --git a/doc/api/managed_licenses.md b/doc/api/managed_licenses.md new file mode 100644 index 00000000000..47b193111b6 --- /dev/null +++ b/doc/api/managed_licenses.md @@ -0,0 +1,136 @@ +# Managed Licenses API **[ULTIMATE]** + +## List managed licenses + +Get all managed licenses for a given project. + +``` +GET /projects/:id/managed_licenses +``` + +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | --------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | + +```bash +curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/managed_licenses +``` + +Example response: + +```json +[ + { + "id": 1, + "name": "MIT", + "approval_status": "approved" + }, + { + "id": 3, + "name": "ISC", + "approval_status": "blacklisted" + } +] +``` + +## Show an existing managed license + +Shows an existing managed license. + +``` +GET /projects/:id/managed_licenses/:managed_license_id +``` + +| Attribute | Type | Required | Description | +| --------------- | ------- | --------------------------------- | ------------------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `managed_license_id` | integer/string | yes | The ID or URL-encoded name of the license belonging to the project | + +```bash +curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/managed_licenses/6" +``` + +Example response: + +```json +{ + "id": 1, + "name": "MIT", + "approval_status": "blacklisted" +} +``` + +## Create a new managed license + +Creates a new managed license for the given project with the given name and approval status. + +``` +POST /projects/:id/managed_licenses +``` + +| Attribute | Type | Required | Description | +| ------------- | ------- | -------- | ---------------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `name` | string | yes | The name of the managed license | +| `approval_status` | string | yes | The approval status. "approved" or "blacklisted" | + +```bash +curl --data "name=MIT&approval_status=blacklisted" --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/managed_licenses" +``` + +Example response: + +```json +{ + "id": 1, + "name": "MIT", + "approval_status": "approved" +} +``` + +## Delete a managed license + +Deletes a managed license with a given id. + +``` +DELETE /projects/:id/managed_licenses/:managed_license_id +``` + +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | --------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `managed_license_id` | integer/string | yes | The ID or URL-encoded name of the license belonging to the project | + +```bash +curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/managed_licenses/4" +``` + +When successful, it replies with an HTTP 204 response. + +## Edit an existing managed license + +Updates an existing managed license with a new approval status. + +``` +PATCH /projects/:id/managed_licenses/:managed_license_id +``` + +| Attribute | Type | Required | Description | +| --------------- | ------- | --------------------------------- | ------------------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `managed_license_id` | integer/string | yes | The ID or URL-encoded name of the license belonging to the project | +| `approval_status` | string | yes | The approval status. "approved" or "blacklisted" | + +```bash +curl --request PATCH --data "approval_status=blacklisted" --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/managed_licenses/6" +``` + +Example response: + +```json +{ + "id": 1, + "name": "MIT", + "approval_status": "blacklisted" +} +``` diff --git a/doc/api/merge_request_approvals.md b/doc/api/merge_request_approvals.md new file mode 100644 index 00000000000..ddac81328b9 --- /dev/null +++ b/doc/api/merge_request_approvals.md @@ -0,0 +1,425 @@ +# Merge request approvals API **[STARTER]** + +Configuration for approvals on all Merge Requests (MR) in the project. Must be authenticated for all endpoints. + +## Project-level MR approvals + +### Get Configuration + +>**Note:** This API endpoint is only available on 10.6 Starter and above. + +You can request information about a project's approval configuration using the +following endpoint: + +``` +GET /projects/:id/approvals +``` + +**Parameters:** + +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | ------------------- | +| `id` | integer | yes | The ID of a project | + +```json +{ + "approvers": [ + { + "user": { + "id": 5, + "name": "John Doe6", + "username": "user5", + "state":"active","avatar_url":"https://www.gravatar.com/avatar/4aea8cf834ed91844a2da4ff7ae6b491?s=80\u0026d=identicon","web_url":"http://localhost/user5" + } + } + ], + "approver_groups": [ + { + "group": { + "id": 1, + "name": "group1", + "path": "group1", + "description": "", + "visibility": "public", + "lfs_enabled": false, + "avatar_url": null, + "web_url": "http://localhost/groups/group1", + "request_access_enabled": false, + "full_name": "group1", + "full_path": "group1", + "parent_id": null, + "ldap_cn": null, + "ldap_access": null + } + } + ], + "approvals_before_merge": 2, + "reset_approvals_on_push": true, + "disable_overriding_approvers_per_merge_request": false +} +``` + +### Change configuration + +>**Note:** This API endpoint is only available on 10.6 Starter and above. + +If you are allowed to, you can change approval configuration using the following +endpoint: + +``` +POST /projects/:id/approvals +``` + +**Parameters:** + +| Attribute | Type | Required | Description | +| ------------------------------------------------ | ------- | -------- | ---------------------------------------------------------- | +| `id` | integer | yes | The ID of a project | +| `approvals_before_merge` | integer | no | How many approvals are required before an MR can be merged | +| `reset_approvals_on_push` | boolean | no | Reset approvals on a new push | +| `disable_overriding_approvers_per_merge_request` | boolean | no | Allow/Disallow overriding approvers per MR | +| `merge_requests_author_approval` | boolean | no | Allow/Disallow authors be able to self approve merge requests | + +```json +{ + "approvers": [ + { + "user": { + "id": 5, + "name": "John Doe6", + "username": "user5", + "state":"active","avatar_url":"https://www.gravatar.com/avatar/4aea8cf834ed91844a2da4ff7ae6b491?s=80\u0026d=identicon","web_url":"http://localhost/user5" + } + } + ], + "approver_groups": [ + { + "group": { + "id": 1, + "name": "group1", + "path": "group1", + "description": "", + "visibility": "public", + "lfs_enabled": false, + "avatar_url": null, + "web_url": "http://localhost/groups/group1", + "request_access_enabled": false, + "full_name": "group1", + "full_path": "group1", + "parent_id": null, + "ldap_cn": null, + "ldap_access": null + } + } + ], + "approvals_before_merge": 2, + "reset_approvals_on_push": true, + "disable_overriding_approvers_per_merge_request": false, + "merge_requests_author_approval": false +} +``` + +### Change allowed approvers + +>**Note:** This API endpoint is only available on 10.6 Starter and above. + +If you are allowed to, you can change approvers and approver groups using +the following endpoint: + +``` +PUT /projects/:id/approvers +``` + +**Important:** Approvers and groups not in the request will be **removed** + +**Parameters:** + +| Attribute | Type | Required | Description | +| -------------------- | ------- | -------- | --------------------------------------------------- | +| `id` | integer | yes | The ID of a project | +| `approver_ids` | Array | yes | An array of User IDs that can approve MRs | +| `approver_group_ids` | Array | yes | An array of Group IDs whose members can approve MRs | + +```json +{ + "approvers": [ + { + "user": { + "id": 5, + "name": "John Doe6", + "username": "user5", + "state":"active","avatar_url":"https://www.gravatar.com/avatar/4aea8cf834ed91844a2da4ff7ae6b491?s=80\u0026d=identicon","web_url":"http://localhost/user5" + } + } + ], + "approver_groups": [ + { + "group": { + "id": 1, + "name": "group1", + "path": "group1", + "description": "", + "visibility": "public", + "lfs_enabled": false, + "avatar_url": null, + "web_url": "http://localhost/groups/group1", + "request_access_enabled": false, + "full_name": "group1", + "full_path": "group1", + "parent_id": null, + "ldap_cn": null, + "ldap_access": null + } + } + ], + "approvals_before_merge": 2, + "reset_approvals_on_push": true, + "disable_overriding_approvers_per_merge_request": false +} +``` + + +## Merge Request-level MR approvals + +Configuration for approvals on a specific Merge Request. Must be authenticated for all endpoints. + +### Get Configuration + +>**Note:** This API endpoint is only available on 8.9 Starter and above. + +You can request information about a merge request's approval status using the +following endpoint: + +``` +GET /projects/:id/merge_requests/:merge_request_iid/approvals +``` + +**Parameters:** + +| Attribute | Type | Required | Description | +|---------------------|---------|----------|---------------------| +| `id` | integer | yes | The ID of a project | +| `merge_request_iid` | integer | yes | The IID of MR | + +```json +{ + "id": 5, + "iid": 5, + "project_id": 1, + "title": "Approvals API", + "description": "Test", + "state": "opened", + "created_at": "2016-06-08T00:19:52.638Z", + "updated_at": "2016-06-08T21:20:42.470Z", + "merge_status": "cannot_be_merged", + "approvals_required": 2, + "approvals_left": 1, + "approved_by": [ + { + "user": { + "name": "Administrator", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon", + "web_url": "http://localhost:3000/u/root" + } + } + ], + "approvers": [], + "approver_groups": [] +} +``` + +### Change approval configuration + +>**Note:** This API endpoint is only available on 10.6 Starter and above. + +If you are allowed to, you can change `approvals_required` using the following +endpoint: + +``` +POST /projects/:id/merge_requests/:merge_request_iid/approvals +``` + +**Parameters:** + +| Attribute | Type | Required | Description | +|----------------------|---------|----------|--------------------------------------------| +| `id` | integer | yes | The ID of a project | +| `merge_request_iid` | integer | yes | The IID of MR | +| `approvals_required` | integer | yes | Approvals required before MR can be merged | + + +```json +{ + "id": 5, + "iid": 5, + "project_id": 1, + "title": "Approvals API", + "description": "Test", + "state": "opened", + "created_at": "2016-06-08T00:19:52.638Z", + "updated_at": "2016-06-08T21:20:42.470Z", + "merge_status": "cannot_be_merged", + "approvals_required": 2, + "approvals_left": 2, + "approved_by": [], + "approvers": [], + "approver_groups": [] +} +``` + +### Change allowed approvers for Merge Request + +>**Note:** This API endpoint is only available on 10.6 Starter and above. + +If you are allowed to, you can change approvers and approver groups using +the following endpoint: + +``` +PUT /projects/:id/merge_requests/:merge_request_iid/approvers +``` + +**Important:** Approvers and groups not in the request will be **removed** + +**Parameters:** + +| Attribute | Type | Required | Description | +|----------------------|---------|----------|-----------------------------------------------------------| +| `id` | integer | yes | The ID of a project | +| `merge_request_iid` | integer | yes | The IID of MR | +| `approver_ids` | Array | yes | An array of User IDs that can approve the MR | +| `approver_group_ids` | Array | yes | An array of Group IDs whose members can approve the MR | + +```json +{ + "id": 5, + "iid": 5, + "project_id": 1, + "title": "Approvals API", + "description": "Test", + "state": "opened", + "created_at": "2016-06-08T00:19:52.638Z", + "updated_at": "2016-06-08T21:20:42.470Z", + "merge_status": "cannot_be_merged", + "approvals_required": 2, + "approvals_left": 2, + "approved_by": [], + "approvers": [ + { + "user": { + "name": "Administrator", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon", + "web_url": "http://localhost:3000/u/root" + } + } + ], + "approver_groups": [ + { + "group": { + "id": 5, + "name": "group1", + "path": "group1", + "description": "", + "visibility": "public", + "lfs_enabled": false, + "avatar_url": null, + "web_url": "http://localhost/groups/group1", + "request_access_enabled": false, + "full_name": "group1", + "full_path": "group1", + "parent_id": null, + "ldap_cn": null, + "ldap_access": null + } + } + ] +} +``` + +## Approve Merge Request + +>**Note:** This API endpoint is only available on 8.9 Starter and above. + +If you are allowed to, you can approve a merge request using the following +endpoint: + +``` +POST /projects/:id/merge_requests/:merge_request_iid/approve +``` + +**Parameters:** + +| Attribute | Type | Required | Description | +|---------------------|---------|----------|-------------------------| +| `id` | integer | yes | The ID of a project | +| `merge_request_iid` | integer | yes | The IID of MR | +| `sha` | string | no | The HEAD of the MR | +| `approval_password` **[STARTER]** | string | no | Current user's password. Required if [**Require user password to approve**](../user/project/merge_requests/merge_request_approvals.md#require-authentication-when-approving-a-merge-request-starter) is enabled in the project settings. | + +The `sha` parameter works in the same way as +when [accepting a merge request](merge_requests.md#accept-mr): if it is passed, then it must +match the current HEAD of the merge request for the approval to be added. If it +does not match, the response code will be `409`. + +```json +{ + "id": 5, + "iid": 5, + "project_id": 1, + "title": "Approvals API", + "description": "Test", + "state": "opened", + "created_at": "2016-06-08T00:19:52.638Z", + "updated_at": "2016-06-09T21:32:14.105Z", + "merge_status": "can_be_merged", + "approvals_required": 2, + "approvals_left": 0, + "approved_by": [ + { + "user": { + "name": "Administrator", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon", + "web_url": "http://localhost:3000/u/root" + } + }, + { + "user": { + "name": "Nico Cartwright", + "username": "ryley", + "id": 2, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/cf7ad14b34162a76d593e3affca2adca?s=80\u0026d=identicon", + "web_url": "http://localhost:3000/u/ryley" + } + } + ], + "approvers": [], + "approver_groups": [] +} +``` + +## Unapprove Merge Request + +>**Note:** This API endpoint is only available on 9.0 Starter and above. + +If you did approve a merge request, you can unapprove it using the following +endpoint: + +``` +POST /projects/:id/merge_requests/:merge_request_iid/unapprove +``` + +**Parameters:** + +| Attribute | Type | Required | Description | +|---------------------|---------|----------|---------------------| +| `id` | integer | yes | The ID of a project | +| `merge_request_iid` | integer | yes | The IID of MR | diff --git a/doc/api/packages.md b/doc/api/packages.md new file mode 100644 index 00000000000..618e5c3056a --- /dev/null +++ b/doc/api/packages.md @@ -0,0 +1,152 @@ +# Packages API **[PREMIUM]** + +This is the API docs of [GitLab Packages](../administration/packages.md). + +## List project packages + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/9259) in GitLab 11.8. + +Get a list of project packages. Both Maven and NPM packages are included in results. +When accessed without authentication, only packages of public projects are returned. + +``` +GET /projects/:id/packages +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | + +```bash +curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/:id/packages +``` + +Example response: + +```json +[ + { + "id": 1, + "name": "com/mycompany/my-app", + "version": "1.0-SNAPSHOT", + "package_type": "maven" + }, + { + "id": 2, + "name": "@foo/bar", + "version": "1.0.3", + "package_type": "npm" + } +] +``` + +By default, the `GET` request will return 20 results, since the API is [paginated](README.md#pagination). + +## Get a project package + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/9667) in GitLab 11.9. + +Get a single project package. + +``` +GET /projects/:id/packages/:package_id +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `package_id` | integer | yes | ID of a package. | + +```bash +curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/:id/packages/:package_id +``` + +Example response: + +```json +{ + "id": 1, + "name": "com/mycompany/my-app", + "version": "1.0-SNAPSHOT", + "package_type": "maven" +} +``` + +## List package files + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/9305) in GitLab 11.8. + +Get a list of package files of a single package. + +``` +GET /projects/:id/packages/:package_id/package_files +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | +| `package_id` | integer | yes | ID of a package. | + +```bash +curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/packages/4/package_files +``` + +Example response: + +```json +[ + { + "id": 25, + "package_id": 4, + "created_at": "2018-11-07T15:25:52.199Z", + "file_name": "my-app-1.5-20181107.152550-1.jar", + "size": 2421, + "file_md5": "58e6a45a629910c6ff99145a688971ac", + "file_sha1": "ebd193463d3915d7e22219f52740056dfd26cbfe" + }, + { + "id": 26, + "package_id": 4, + "created_at": "2018-11-07T15:25:56.776Z", + "file_name": "my-app-1.5-20181107.152550-1.pom", + "size": 1122, + "file_md5": "d90f11d851e17c5513586b4a7e98f1b2", + "file_sha1": "9608d068fe88aff85781811a42f32d97feb440b5" + }, + { + "id": 27, + "package_id": 4, + "created_at": "2018-11-07T15:26:00.556Z", + "file_name": "maven-metadata.xml", + "size": 767, + "file_md5": "6dfd0cce1203145a927fef5e3a1c650c", + "file_sha1": "d25932de56052d320a8ac156f745ece73f6a8cd2" + } +] +``` + +By default, the `GET` request will return 20 results, since the API is [paginated](README.md#pagination). + +## Delete a project package + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/9623) in GitLab 11.9. + +Deletes a project package. + +``` +DELETE /projects/:id/packages/:package_id +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | +| `package_id` | integer | yes | ID of a package. | + +```bash +curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/:id/packages/:package_id +``` + +Can return the following status codes: + +- `204 No Content`, if the package was deleted successfully. +- `404 Not Found`, if the package was not found. diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md index 6fcc06ea8cd..87c7f371de1 100644 --- a/doc/api/repository_files.md +++ b/doc/api/repository_files.md @@ -181,7 +181,7 @@ Currently gitlab-shell has a boolean return code, preventing GitLab from specify ## Delete existing file in repository -This allows you to delete a single file. For deleting multiple files with a singleh request see the [commits API](commits.html#create-a-commit-with-multiple-files-and-actions). +This allows you to delete a single file. For deleting multiple files with a single request, see the [commits API](commits.html#create-a-commit-with-multiple-files-and-actions). ``` DELETE /projects/:id/repository/files/:file_path diff --git a/doc/api/scim.md b/doc/api/scim.md new file mode 100644 index 00000000000..4595c6f2ed3 --- /dev/null +++ b/doc/api/scim.md @@ -0,0 +1,235 @@ +# SCIM API + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/9388) in [GitLab Silver](https://about.gitlab.com/pricing/) 11.10. + +The SCIM API implements the [the RFC7644 protocol](https://tools.ietf.org/html/rfc7644). + +NOTE: **Note:** +[Group SSO](../user/group/saml_sso/index.md) and the feature +flag `:group_scim` must be enabled for the group. For more information, see [SCIM setup documentation](../user/group/saml_sso/scim_setup.md#requirements). + +## Get a list of SAML users + +NOTE: **Note:** +This endpoint is used as part of the SCIM syncing mechanism and it only returns +a single user based on a unique ID which should match the `extern_uid` of the user. + +```text +GET /api/scim/v2/groups/:group_path/Users +``` + +Parameters: + +| Attribute | Type | Required | Description | +|:----------|:--------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------| +| `filter` | string | yes | A [filter](#available-filters) expression. | +| `group_path` | string | yes | Full path to the group. | + +Example request: + +```sh +curl 'https://example.gitlab.com/api/scim/v2/groups/test_group/Users?filter=id%20eq%20"0b1d561c-21ff-4092-beab-8154b17f82f2"' --header "Authorization: Bearer <your_scim_token>" --header "Content-Type: application/scim+json" +``` + +Example response: + +```json +{ + "schemas": [ + "urn:ietf:params:scim:api:messages:2.0:ListResponse" + ], + "totalResults": 1, + "itemsPerPage": 20, + "startIndex": 1, + "Resources": [ + { + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:User" + ], + "id": "0b1d561c-21ff-4092-beab-8154b17f82f2", + "active": true, + "name.formatted": "Test User", + "userName": "username", + "meta": { "resourceType":"User" }, + "emails": [ + { + "type": "work", + "value": "name@example.com", + "primary": true + } + ] + } + ] +} +``` + +## Get a single SAML user + +```text +GET /api/scim/v2/groups/:group_path/Users/:id +``` + +Parameters: + +| Attribute | Type | Required | Description | +|:----------|:--------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------| +| `id` | string | yes | External UID of the user. | +| `group_path` | string | yes | Full path to the group. | + +Example request: + +```sh +curl 'https://example.gitlab.com/api/scim/v2/groups/test_group/Users/f0b1d561c-21ff-4092-beab-8154b17f82f2' --header "Authorization: Bearer <your_scim_token>" --header "Content-Type: application/scim+json" +``` + +Example response: + +```json +{ + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:User" + ], + "id": "0b1d561c-21ff-4092-beab-8154b17f82f2", + "active": true, + "name.formatted": "Test User", + "userName": "username", + "meta": { "resourceType":"User" }, + "emails": [ + { + "type": "work", + "value": "name@example.com", + "primary": true + } + ] +} +``` + +## Create a SAML user + +```text +POST /api/scim/v2/groups/:group_path/Users/ +``` + +Parameters: + +| Attribute | Type | Required | Description | +|:---------------|:----------|:----|:--------------------------| +| `externalId` | string | yes | External UID of the user. | +| `userName` | string | yes | Username of the user. | +| `emails` | JSON string | yes | Work email. | +| `name` | JSON string | yes | Name of the user. | +| `meta` | string | no | Resource type (`User'). | + +Example request: + +```sh +curl --verbose --request POST 'https://example.gitlab.com/api/scim/v2/groups/test_group/Users' --data '{"externalId":"test_uid","active":null,"userName":"username","emails":[{"primary":true,"type":"work","value":"name@example.com"}],"name":{"formatted":"Test User","familyName":"User","givenName":"Test"},"schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],"meta":{"resourceType":"User"}}' --header "Authorization: Bearer <your_scim_token>" --header "Content-Type: application/scim+json" +``` + +Example response: + +```json +{ + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:User" + ], + "id": "0b1d561c-21ff-4092-beab-8154b17f82f2", + "active": true, + "name.formatted": "Test User", + "userName": "username", + "meta": { "resourceType":"User" }, + "emails": [ + { + "type": "work", + "value": "name@example.com", + "primary": true + } + ] +} +``` + +Returns a `201` status code if successful. + +## Update a single SAML user + +Fields that can be updated are: + +| SCIM/IdP field | GitLab field | +|:----------|:--------| +| id/externalId | extern_uid | +| name.formatted | name | +| emails\[type eq "work"\].value | email | +| active | Identity removal if `active = false` | +| userName | username | + +```text +PATCH /api/scim/v2/groups/:group_path/Users/:id +``` + +Parameters: + +| Attribute | Type | Required | Description | +|:----------|:--------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------| +| `id` | string | yes | External UID of the user. | +| `group_path` | string | yes | Full path to the group. | +| `Operations` | JSON string | yes | An [operations](#available-operations) expression. | + +Example request: + +```sh +curl --verbose --request PATCH 'https://example.gitlab.com/api/scim/v2/groups/test_group/Users/f0b1d561c-21ff-4092-beab-8154b17f82f2' --data '{ "Operations": [{"op":"Add","path":"name.formatted","value":"New Name"}] }' --header "Authorization: Bearer <your_scim_token>" --header "Content-Type: application/scim+json" +``` + +Returns an empty response with a `204` status code if successful. + +## Remove a single SAML user + +Removes the user's SSO identity and group membership. + +```text +DELETE /api/scim/v2/groups/:group_path/Users/:id +``` + +Parameters: + +| Attribute | Type | Required | Description | +|:----------|:--------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------| +| `id` | string | yes | External UID of the user. | +| `group_path` | string | yes | Full path to the group. | + +Example request: + +```sh +curl --verbose --request DELETE 'https://example.gitlab.com/api/scim/v2/groups/test_group/Users/f0b1d561c-21ff-4092-beab-8154b17f82f2' --header "Authorization: Bearer <your_scim_token>" --header "Content-Type: application/scim+json" +``` + +Returns an empty response with a `204` status code if successful. + +## Available filters + +They match an expression as specified in [the RFC7644 filtering section](https://tools.ietf.org/html/rfc7644#section-3.4.2.2). + +| Filter | Description | +| ----- | ----------- | +| `eq` | The attribute matches exactly the specified value. | + +Example: + +``` +id eq a-b-c-d +``` + +## Available operations + +They perform an operation as specified in [the RFC7644 update section](https://tools.ietf.org/html/rfc7644#section-3.5.2). + +| Operator | Description | +| ----- | ----------- | +| `Replace` | The attribute's value is updated. | +| `Add` | The attribute has a new value. | + +Example: + +```json +{ "op": "Add", "path": "name.formatted", "value": "New Name" } +``` diff --git a/doc/api/search.md b/doc/api/search.md index 6ee3d32d8bc..c2dd72479c1 100644 --- a/doc/api/search.md +++ b/doc/api/search.md @@ -556,6 +556,7 @@ GET /projects/:id/search | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `scope` | string | yes | The scope to search in | | `search` | string | yes | The search query | +| `ref` | string | no | The name of a repository branch or tag to search on. The project's default branch is used by default. This is only applicable for scopes: commits, blobs, and wiki_blobs. | Search the expression within the specified scope. Currently these scopes are supported: issues, merge_requests, milestones, notes, wiki_blobs, commits, blobs, users. @@ -850,7 +851,7 @@ Blobs searches are performed on both filenames and contents. Search results: times in the content. ```bash -curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/6/search?scope=blobs&search=installation +curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/6/search?scope=blobs&search=installation&ref=feature ``` Example response: @@ -863,7 +864,7 @@ Example response: "data": "```\n\n## Installation\n\nQuick start using the [pre-built", "filename": "README.md", "id": null, - "ref": "master", + "ref": "feature", "startline": 46, "project_id": 6 } diff --git a/doc/api/vulnerabilities.md b/doc/api/vulnerabilities.md new file mode 100644 index 00000000000..87f77613ad3 --- /dev/null +++ b/doc/api/vulnerabilities.md @@ -0,0 +1,113 @@ +# Vulnerabilities API + +Every API call to vulnerabilities must be authenticated. + +If a user is not a member of a project and the project is private, a `GET` +request on that project will result in a `404` status code. + +CAUTION: **Caution:** +This API is in an alpha stage and considered unstable. +The response payload may be subject to change or breakage +across GitLab releases. + +## Vulnerabilities pagination + +By default, `GET` requests return 20 results at a time because the API results +are paginated. + +Read more on [pagination](README.md#pagination). + +## List project vulnerabilities + +List all of a project's vulnerabilities. + +``` +GET /projects/:id/vulnerabilities +GET /projects/:id/vulnerabilities?report_type=sast +GET /projects/:id/vulnerabilities?report_type=container_scanning +GET /projects/:id/vulnerabilities?report_type=sast,dast +GET /projects/:id/vulnerabilities?scope=all +GET /projects/:id/vulnerabilities?scope=dismissed +GET /projects/:id/vulnerabilities?severity=high +GET /projects/:id/vulnerabilities?confidence=unknown,experimental +``` + +| Attribute | Type | Required | Description | +| ------------------- | ---------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `report_type` | Array[string] | no | Returns vulnerabilities belonging to specified report type. Valid values: `sast`, `dast`, `dependency_scanning`, or `container_scanning`. | +| `scope` | string | no | Returns vulnerabilities for the given scope: `all` or `dismissed`. Defaults to `dismissed` | +| `severity` | Array[string] | no | Returns vulnerabilities belonging to specified severity level: `undefined`, `info`, `unknown`, `low`, `medium`, `high`, or `critical`. Defaults to all' | +| `confidence` | Array[string] | no | Returns vulnerabilities belonging to specified confidence level: `undefined`, `ignore`, `unknown`, `experimental`, `low`, `medium`, `high`, or `confirmed`. Defaults to all | + +```bash +curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/4/vulnerabilities +``` + +Example response: + +```json +[ + { + "id": null, + "report_type": "dependency_scanning", + "name": "Authentication bypass via incorrect DOM traversal and canonicalization in saml2-js", + "severity": "unknown", + "confidence": "undefined", + "scanner": { + "external_id": "gemnasium", + "name": "Gemnasium" + }, + "identifiers": [ + { + "external_type": "gemnasium", + "external_id": "9952e574-7b5b-46fa-a270-aeb694198a98", + "name": "Gemnasium-9952e574-7b5b-46fa-a270-aeb694198a98", + "url": "https://deps.sec.gitlab.com/packages/npm/saml2-js/versions/1.5.0/advisories" + }, + { + "external_type": "cve", + "external_id": "CVE-2017-11429", + "name": "CVE-2017-11429", + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-11429" + } + ], + "project_fingerprint": "fa6f5b6c5d240b834ac5e901dc69f9484cef89ec", + "create_vulnerability_feedback_issue_path": "/tests/yarn-remediation-test/vulnerability_feedback", + "create_vulnerability_feedback_merge_request_path": "/tests/yarn-remediation-test/vulnerability_feedback", + "create_vulnerability_feedback_dismissal_path": "/tests/yarn-remediation-test/vulnerability_feedback", + "project": { + "id": 31, + "name": "yarn-remediation-test", + "full_path": "/tests/yarn-remediation-test", + "full_name": "tests / yarn-remediation-test" + }, + "dismissal_feedback": null, + "issue_feedback": null, + "merge_request_feedback": null, + "description": "Some XML DOM traversal and canonicalization APIs may be inconsistent in handling of comments within XML nodes. Incorrect use of these APIs by some SAML libraries results in incorrect parsing of the inner text of XML nodes such that any inner text after the comment is lost prior to cryptographically signing the SAML message. Text after the comment therefore has no impact on the signature on the SAML message.\r\n\r\nA remote attacker can modify SAML content for a SAML service provider without invalidating the cryptographic signature, which may allow attackers to bypass primary authentication for the affected SAML service provider.", + "links": [ + { + "url": "https://github.com/Clever/saml2/commit/3546cb61fd541f219abda364c5b919633609ef3d#diff-af730f9f738de1c9ad87596df3f6de84R279" + }, + { + "url": "https://www.kb.cert.org/vuls/id/475445" + }, + { + "url": "https://github.com/Clever/saml2/issues/127" + } + ], + "location": { + "file": "yarn.lock", + "dependency": { + "package": { + "name": "saml2-js" + }, + "version": "1.5.0" + } + }, + "solution": "Upgrade to fixed version.\r\n", + "blob_path": "/tests/yarn-remediation-test/blob/cc6c4a0778460455ae5d16ca7025ca9ca1ca75ac/yarn.lock" + } +] +``` diff --git a/doc/ci/caching/index.md b/doc/ci/caching/index.md index e079483e2b5..3f72fe3e9eb 100644 --- a/doc/ci/caching/index.md +++ b/doc/ci/caching/index.md @@ -289,7 +289,7 @@ jobs inherit it. Gems are installed in `vendor/ruby/` and are cached per-branch: # # https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml # -image: ruby:2.5 +image: ruby:2.6 # Cache gems in between builds cache: diff --git a/doc/ci/ci_cd_for_external_repos/img/github_omniauth_list.png b/doc/ci/ci_cd_for_external_repos/img/github_omniauth_list.png Binary files differdeleted file mode 100644 index 3f2059504f5..00000000000 --- a/doc/ci/ci_cd_for_external_repos/img/github_omniauth_list.png +++ /dev/null diff --git a/doc/ci/environments.md b/doc/ci/environments.md index d5e6fbe8113..5a14ac17aec 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -268,7 +268,7 @@ For the value of: which receives the value of the branch name. - `environment:url`, we want a specific and distinct URL for each branch. `$CI_COMMIT_REF_NAME` may contain a `/` or other characters that would be invalid in a domain name or URL, - so we use `$CI_ENVIRONMENT_SLUG` to get a "clean" or "safe" URL. + so we use `$CI_ENVIRONMENT_SLUG` to guarantee that we get a valid URL. For example, given a `$CI_COMMIT_REF_NAME` of `100-Do-The-Thing`, the URL will be something like `https://100-do-the-4f99a2.example.com`. Again, the way you set up @@ -351,7 +351,7 @@ deploy_prod: ``` A more realistic example would also include copying files to a location where a -webserver (for example, NGINX) could then acess and serve them. +webserver (for example, NGINX) could then access and serve them. The example below will copy the `public` directory to `/srv/nginx/$CI_COMMIT_REF_SLUG/public`: diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md index 6b9f8181d1d..340a41c196b 100644 --- a/doc/ci/examples/README.md +++ b/doc/ci/examples/README.md @@ -18,30 +18,30 @@ Examples are available in several forms. As a collection of: The following table lists examples for different use cases: -| Use case | Resource | -|:-----------------------------------------------|:---------------------------------------------------------------------------------------------------------------------| -| Browser performance testing | [Browser Performance Testing with the Sitespeed.io container](browser_performance.md). | -| Clojure | [Test a Clojure application with GitLab CI/CD](test-clojure-application.md). | -| Code quality analysis | [Analyze your project's Code Quality](code_quality.md). **[STARTER]** | -| Container scanning | [Container Scanning with GitLab CI/CD](container_scanning.md). | -| Dependency scanning | [Dependency Scanning with GitLab CI/CD](dependency_scanning.md). **[ULTIMATE]** | -| Deployment with `dpl` | [Using `dpl` as deployment tool](deployment/README.md). | -| Dynamic application<br>security testing (DAST) | [Dynamic Application Security Testing with GitLab CI/CD](dast.md) **[ULTIMATE]** | -| Elixir | [Testing a Phoenix application with GitLab CI/CD](test_phoenix_app_with_gitlab_ci_cd/index.md). | -| Game development | [DevOps and Game Dev with GitLab CI/CD](devops_and_game_dev_with_gitlab_ci_cd/index.md). | -| GitLab Pages | See the [GitLab Pages](../../user/project/pages/index.md) documentation for a complete example. | -| Java | [Deploy a Spring Boot application to Cloud Foundry with GitLab CI/CD](deploy_spring_boot_to_cloud_foundry/index.md). | -| JUnit | [JUnit test reports](../junit_test_reports.md). | -| License management | [Dependencies license management with GitLab CI/CD](license_management.md) **[ULTIMATE]** | -| Maven | [How to deploy Maven projects to Artifactory with GitLab CI/CD](artifactory_and_gitlab/index.md). | -| PHP | [Testing PHP projects](php.md). | -| PHP | [Running Composer and NPM scripts with deployment via SCP in GitLab CI/CD](deployment/composer-npm-deploy.md). | -| PHP | [Test and deploy Laravel applications with GitLab CI/CD and Envoy](laravel_with_gitlab_and_envoy/index.md). | -| Python | [Test and deploy a Python application with GitLab CI/CD](test-and-deploy-python-application-to-heroku.md). | -| Ruby | [Test and deploy a Ruby application with GitLab CI/CD](test-and-deploy-ruby-application-to-heroku.md). | -| Scala | [Test and deploy a Scala application to Heroku](test-scala-application.md). | -| Static application<br>security testing (SAST) | [Static Application Security Testing with GitLab CI/CD](sast.md) **[ULTIMATE]** | -| Testing | [End-to-end testing with GitLab CI/CD and WebdriverIO](end_to_end_testing_webdriverio/index.md). | +| Use case | Resource | +|:-----------------------------------------------|:------------------------------------------------------------------------------------------------------------------------| +| Browser performance testing | [Browser Performance Testing with the Sitespeed.io container](browser_performance.md). | +| Clojure | [Test a Clojure application with GitLab CI/CD](test-clojure-application.md). | +| Code quality analysis | [Analyze your project's Code Quality](code_quality.md). **[STARTER]** | +| Container scanning | [Container Scanning with GitLab CI/CD](../../user/application_security/container_scanning/index.md). **[ULTIMATE]** | +| Dependency scanning | [Dependency Scanning with GitLab CI/CD](../../user/application_security/dependency_scanning/index.md). **[ULTIMATE]** | +| Deployment with `dpl` | [Using `dpl` as deployment tool](deployment/README.md). | +| Dynamic application<br>security testing (DAST) | [Dynamic Application Security Testing with GitLab CI/CD](../../user/application_security/dast/index.md). **[ULTIMATE]** | +| Elixir | [Testing a Phoenix application with GitLab CI/CD](test_phoenix_app_with_gitlab_ci_cd/index.md). | +| Game development | [DevOps and Game Dev with GitLab CI/CD](devops_and_game_dev_with_gitlab_ci_cd/index.md). | +| GitLab Pages | See the [GitLab Pages](../../user/project/pages/index.md) documentation for a complete example. | +| Java | [Deploy a Spring Boot application to Cloud Foundry with GitLab CI/CD](deploy_spring_boot_to_cloud_foundry/index.md). | +| JUnit | [JUnit test reports](../junit_test_reports.md). | +| License management | [Dependencies license management with GitLab CI/CD](../../user/application_security/license_management/index.md). **[ULTIMATE]** | +| Maven | [How to deploy Maven projects to Artifactory with GitLab CI/CD](artifactory_and_gitlab/index.md). | +| PHP | [Testing PHP projects](php.md). | +| PHP | [Running Composer and NPM scripts with deployment via SCP in GitLab CI/CD](deployment/composer-npm-deploy.md). | +| PHP | [Test and deploy Laravel applications with GitLab CI/CD and Envoy](laravel_with_gitlab_and_envoy/index.md). | +| Python | [Test and deploy a Python application with GitLab CI/CD](test-and-deploy-python-application-to-heroku.md). | +| Ruby | [Test and deploy a Ruby application with GitLab CI/CD](test-and-deploy-ruby-application-to-heroku.md). | +| Scala | [Test and deploy a Scala application to Heroku](test-scala-application.md). | +| Static application<br>security testing (SAST) | [Static Application Security Testing with GitLab CI/CD](../../user/application_security/sast/index.md). **[ULTIMATE]** | +| Testing | [End-to-end testing with GitLab CI/CD and WebdriverIO](end_to_end_testing_webdriverio/index.md). | ### Contributing examples diff --git a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md index cf281605f5e..c622dd86828 100644 --- a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md +++ b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md @@ -4,6 +4,7 @@ author_gitlab: DylanGriffith level: intermediate article_type: tutorial date: 2018-06-07 +last_updated: 2019-04-08 description: "Continuous Deployment of a Spring Boot application to Cloud Foundry with GitLab CI/CD" --- @@ -77,7 +78,10 @@ image: java:8 stages: - build - deploy - + +before_script: + - chmod +x mvnw + build: stage: build script: ./mvnw package diff --git a/doc/ci/examples/laravel_with_gitlab_and_envoy/img/laravel_with_gitlab_and_envoy.png b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/laravel_with_gitlab_and_envoy.png Binary files differdeleted file mode 100644 index bc188f83fb1..00000000000 --- a/doc/ci/examples/laravel_with_gitlab_and_envoy/img/laravel_with_gitlab_and_envoy.png +++ /dev/null diff --git a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md index 99a4316ab0d..47d20a4e1c1 100644 --- a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md +++ b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md @@ -9,7 +9,12 @@ You can checkout the [example source](https://gitlab.com/ayufan/python-getting-s This is what the `.gitlab-ci.yml` file looks like for this project: ```yaml +stages: + - test + - deploy + test: + stage: test script: # this configures Django application to use attached postgres database that is run on `postgres` host - export DATABASE_URL=postgres://postgres:@postgres:5432/python-test-app @@ -19,7 +24,7 @@ test: - python manage.py test staging: - type: deploy + stage: deploy script: - apt-get update -qy - apt-get install -y ruby-dev @@ -29,7 +34,7 @@ staging: - master production: - type: deploy + stage: deploy script: - apt-get update -qy - apt-get install -y ruby-dev diff --git a/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/img/job-succeeded.png b/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/img/job-succeeded.png Binary files differdeleted file mode 100644 index 77b05f55f88..00000000000 --- a/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/img/job-succeeded.png +++ /dev/null diff --git a/doc/ci/introduction/index.md b/doc/ci/introduction/index.md index 14ea648c00b..0045fa2fb1f 100644 --- a/doc/ci/introduction/index.md +++ b/doc/ci/introduction/index.md @@ -156,7 +156,7 @@ Once you're happy with your implementation: ![GitLab workflow example](img/gitlab_workflow_example_11_9.png) -GitLab CI/CD is capable of a doing a lot more, but this workflow +GitLab CI/CD is capable of doing a lot more, but this workflow exemplifies GitLab's ability to track the entire process, without the need of any external tool to deliver your software. And, most usefully, you can visualize all the steps through diff --git a/doc/ci/merge_request_pipelines/img/pipeline_detail.png b/doc/ci/merge_request_pipelines/img/pipeline_detail.png Binary files differdeleted file mode 100644 index 90e7c449a66..00000000000 --- a/doc/ci/merge_request_pipelines/img/pipeline_detail.png +++ /dev/null diff --git a/doc/ci/merge_request_pipelines/index.md b/doc/ci/merge_request_pipelines/index.md index 3c26a38e3de..b3ff55daea2 100644 --- a/doc/ci/merge_request_pipelines/index.md +++ b/doc/ci/merge_request_pipelines/index.md @@ -70,15 +70,18 @@ when a merge request was created or updated. For example: ## Pipelines for Merged Results **[PREMIUM]** > [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/7380) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.10. +> This feature is disabled by default until we resolve issues with [contention handling](https://gitlab.com/gitlab-org/gitlab-ee/issues/9186), but [can be enabled manually](#enabling-pipelines-for-merged-results). It's possible for your source and target branches to diverge, which can result in the scenario that source branch's pipeline was green, the target's pipeline was green, -but the combined output fails. By having your merge request pipeline automatically +but the combined output fails. + +By having your merge request pipeline automatically create a new ref that contains the merge result of the source and target branch (then running a pipeline on that ref), we can better test that the combined result is also valid. -From GitLab 11.10, pipelines for merge requests run by default +GitLab can run pipelines for merge requests on this merged result. That is, where the source and target branches are combined into a new ref and a pipeline for this ref validates the result prior to merging. @@ -95,7 +98,7 @@ get out of WIP status or resolve merge conflicts as soon as possible. ### Enabling Pipelines for Merged Results -This feature disabled by default until we resolve issues with [contention handling](https://gitlab.com/gitlab-org/gitlab-ee/issues/9186). It can be enabled at the project level: +To enable pipelines on merged results at the project level: 1. Visit your project's **Settings > General** and expand **Merge requests**. 1. Check **Merge pipelines will try to validate the post-merge result prior to merging**. @@ -103,6 +106,10 @@ This feature disabled by default until we resolve issues with [contention handli ![Merge request pipeline config](img/merge_request_pipeline_config.png) +CAUTION: **Warning:** +Make sure your `gitlab-ci.yml` file is [configured properly for pipelines for merge requests](#configuring-pipelines-for-merge-requests), +otherwise pipelines for merged results won't run and your merge requests will be stuck in an unresolved state. + ### Pipelines for Merged Result's limitations - This feature requires [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner) 11.9 or newer. @@ -193,5 +200,5 @@ By using pipelines for merge requests, GitLab exposes additional predefined vari Those variables contain information of the associated merge request, so that it's useful to integrate your job with [GitLab Merge Request API](../../api/merge_requests.md). -You can find the list of avilable variables in [the reference sheet](../variables/predefined_variables.md). +You can find the list of available variables in [the reference sheet](../variables/predefined_variables.md). The variable names begin with the `CI_MERGE_REQUEST_` prefix. diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md index 342c2ab972a..4dbe1a85588 100644 --- a/doc/ci/pipelines.md +++ b/doc/ci/pipelines.md @@ -330,6 +330,19 @@ GitLab provides API endpoints to: - [Triggering pipelines through the API](triggers/README.md). - [Pipeline triggers API](../api/pipeline_triggers.md). +### Start multiple manual actions in a stage + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/27188) in GitLab 11.11. + +Multiple manual actions in a single stage can be started at the same time using the "Play all manual" button. +Once the user clicks this button, each individual manual action will be triggered and refreshed +to an updated status. + +This functionality is only available: + +- For users with at least Developer access. +- If the the stage contains [manual actions](#manual-actions-from-pipeline-graphs). + ## Security on protected branches A strict security model is enforced when pipelines are executed on diff --git a/doc/ci/runners/shared_to_specific_admin.png b/doc/ci/runners/shared_to_specific_admin.png Binary files differdeleted file mode 100644 index 8f4010a5849..00000000000 --- a/doc/ci/runners/shared_to_specific_admin.png +++ /dev/null diff --git a/doc/ci/services/mysql.md b/doc/ci/services/mysql.md index 2902c30c7c0..5fa378fc4c2 100644 --- a/doc/ci/services/mysql.md +++ b/doc/ci/services/mysql.md @@ -17,8 +17,8 @@ services: variables: # Configure mysql environment variables (https://hub.docker.com/_/mysql/) - MYSQL_DATABASE: el_duderino - MYSQL_ROOT_PASSWORD: mysql_strong_password + MYSQL_DATABASE: "<your_mysql_database>" + MYSQL_ROOT_PASSWORD: "<your_mysql_password>" ``` And then configure your application to use the database, for example: @@ -26,18 +26,18 @@ And then configure your application to use the database, for example: ```yaml Host: mysql User: root -Password: mysql_strong_password -Database: el_duderino +Password: <your_mysql_password> +Database: <your_mysql_database> ``` If you are wondering why we used `mysql` for the `Host`, read more at [How services are linked to the job](../docker/using_docker_images.md#how-services-are-linked-to-the-job). -You can also use any other docker image available on [Docker Hub][hub-mysql]. +You can also use any other docker image available on [Docker Hub](https://hub.docker.com/_/mysql/). For example, to use MySQL 5.5 the service becomes `mysql:5.5`. The `mysql` image can accept some environment variables. For more details -check the documentation on [Docker Hub][hub-mysql]. +check the documentation on [Docker Hub](https://hub.docker.com/_/mysql/). ## Use MySQL with the Shell executor @@ -74,13 +74,13 @@ mysql> CREATE USER 'runner'@'localhost' IDENTIFIED BY '$password'; Create the database: ```bash -mysql> CREATE DATABASE IF NOT EXISTS `el_duderino` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_unicode_ci`; +mysql> CREATE DATABASE IF NOT EXISTS `<your_mysql_database>` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_unicode_ci`; ``` Grant the necessary permissions on the database: ```bash -mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, CREATE TEMPORARY TABLES, DROP, INDEX, ALTER, LOCK TABLES ON `el_duderino`.* TO 'runner'@'localhost'; +mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, CREATE TEMPORARY TABLES, DROP, INDEX, ALTER, LOCK TABLES ON `<your_mysql_database>`.* TO 'runner'@'localhost'; ``` If all went well you can now quit the database session: @@ -93,7 +93,7 @@ Now, try to connect to the newly created database to check that everything is in place: ```bash -mysql -u runner -p -D el_duderino +mysql -u runner -p -D <your_mysql_database> ``` As a final step, configure your application to use the database, for example: @@ -102,17 +102,14 @@ As a final step, configure your application to use the database, for example: Host: localhost User: runner Password: $password -Database: el_duderino +Database: <your_mysql_database> ``` ## Example project -We have set up an [Example MySQL Project][mysql-example-repo] for your +We have set up an [Example MySQL Project](https://gitlab.com/gitlab-examples/mysql) for your convenience that runs on [GitLab.com](https://gitlab.com) using our publicly available [shared runners](../runners/README.md). -Want to hack on it? Simply fork it, commit and push your changes. Within a few +Want to hack on it? Simply fork it, commit and push your changes. Within a few moments the changes will be picked by a public runner and the job will begin. - -[hub-mysql]: https://hub.docker.com/_/mysql/ -[mysql-example-repo]: https://gitlab.com/gitlab-examples/mysql diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 99e4c64ff86..31ff56e06f8 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -337,6 +337,7 @@ In addition, `only` and `except` allow the use of special keywords: | `triggers` | For pipelines created using a trigger token. | | `web` | For pipelines created using **Run pipeline** button in GitLab UI (under your project's **Pipelines**). | | `merge_requests` | When a merge request is created or updated (See [pipelines for merge requests](../merge_request_pipelines/index.md)). | +| `chats` | For jobs created using a [GitLab ChatOps](../chatops/README.md) command. | In the example below, `job` will run only for refs that start with `issue-`, whereas all branches will be skipped: @@ -1446,7 +1447,7 @@ be automatically shown in merge requests. > Introduced in GitLab 11.5. Requires GitLab Runner 11.5 and above. -The `sast` report collects [SAST vulnerabilities](https://docs.gitlab.com/ee/user/project/merge_requests/sast.html) +The `sast` report collects [SAST vulnerabilities](https://docs.gitlab.com/ee/user/application_security/sast/index.html) as artifacts. The collected SAST report will be uploaded to GitLab as an artifact and will @@ -1509,6 +1510,8 @@ be automatically shown in merge requests. ##### `artifacts:reports:metrics` **[PREMIUM]** +> Introduced in GitLab 11.10. + The `metrics` report collects [Metrics](../../ci/metrics_reports.md) as artifacts. @@ -1764,9 +1767,6 @@ TIP: **Tip:** Use merging to customize and override included CI/CD configurations with local definitions. -Recursive includes are not supported. Your external files should not use the -`include` keyword as it will be ignored. - NOTE: **Note:** Using YAML aliases across different YAML files sourced by `include` is not supported. You must only refer to aliases in the same file. Instead @@ -2078,8 +2078,8 @@ of the `group/my-project`: ```yaml include: - - local: : /templates/docker-build.yml - - local: : /templates/docker-testing.yml + - local: /templates/docker-build.yml + - local: /templates/docker-testing.yml ``` Our `/templates/docker-build.yml` present in `group/my-project` adds a `docker-build` job: diff --git a/doc/development/README.md b/doc/development/README.md index 83a1145c020..2ff38d68a47 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -44,6 +44,7 @@ description: 'Learn how to contribute to GitLab.' - [`Gemfile` guidelines](gemfile.md) - [Pry debugging](pry_debugging.md) - [Sidekiq debugging](sidekiq_debugging.md) +- [Accessing session data](session.md) - [Gotchas](gotchas.md) to avoid - [Avoid modules with instance variables](module_with_instance_variables.md) if possible - [How to dump production data to staging](db_dump.md) @@ -136,3 +137,8 @@ description: 'Learn how to contribute to GitLab.' ## Go guides - [Go Guidelines](go_guide/index.md) + +## Other GitLab Development Kit (GDK) guides + +- [Run full Auto DevOps cycle in a GDK instance](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/auto_devops.md) +- [Using GitLab Runner with GDK](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/runner.md) diff --git a/doc/development/architecture.md b/doc/development/architecture.md index 115c8cfb9ff..9a012f4299b 100644 --- a/doc/development/architecture.md +++ b/doc/development/architecture.md @@ -10,34 +10,147 @@ For information, see the [GitLab Release Process](https://gitlab.com/gitlab-org/ Both EE and CE require some add-on components called gitlab-shell and Gitaly. These components are available from the [gitlab-shell](https://gitlab.com/gitlab-org/gitlab-shell/tree/master) and [gitaly](https://gitlab.com/gitlab-org/gitaly/tree/master) repositories respectively. New versions are usually tags but staying on the master branch will give you the latest stable version. New releases are generally around the same time as GitLab CE releases with exception for informal security updates deemed critical. -## GitLab Omnibus Component by Component +## Components -This document is designed to be consumed by systems adminstrators and GitLab Support Engineers who want to understand more about the internals of GitLab and how they work together. +A typical install of GitLab will be on GNU/Linux. It uses Nginx or Apache as a web front end to proxypass the Unicorn web server. By default, communication between Unicorn and the front end is via a Unix domain socket but forwarding requests via TCP is also supported. The web front end accesses `/home/git/gitlab/public` bypassing the Unicorn server to serve static pages, uploads (e.g. avatar images or attachments), and precompiled assets. GitLab serves web pages and a [GitLab API](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/api) using the Unicorn web server. It uses Sidekiq as a job queue which, in turn, uses redis as a non-persistent database backend for job information, meta data, and incoming jobs. -When deployed, GitLab should be considered the amalgamation of the below processes. When troubleshooting or debugging, be as specific as possible as to which component you are referencing. That should increase clarity and reduce confusion. +We also support deploying GitLab on Kubernetes using our [gitlab Helm chart](https://docs.gitlab.com/charts/). + +The GitLab web app uses MySQL or PostgreSQL for persistent database information (e.g. users, permissions, issues, other meta data). GitLab stores the bare git repositories it serves in `/home/git/repositories` by default. It also keeps default branch and hook information with the bare repository. -### GitLab Process Descriptions +When serving repositories over HTTP/HTTPS GitLab utilizes the GitLab API to resolve authorization and access as well as serving git objects. -As of this writing, a fresh GitLab 11.3.0 install will show the following processes with `gitlab-ctl status`: +The add-on component gitlab-shell serves repositories over SSH. It manages the SSH keys within `/home/git/.ssh/authorized_keys` which should not be manually edited. gitlab-shell accesses the bare repositories through Gitaly to serve git objects and communicates with redis to submit jobs to Sidekiq for GitLab to process. gitlab-shell queries the GitLab API to determine authorization and access. -``` -run: alertmanager: (pid 30829) 14207s; run: log: (pid 13906) 2432044s -run: gitaly: (pid 30771) 14210s; run: log: (pid 13843) 2432046s -run: gitlab-monitor: (pid 30788) 14209s; run: log: (pid 13868) 2432045s -run: gitlab-workhorse: (pid 30758) 14210s; run: log: (pid 13855) 2432046s -run: logrotate: (pid 30246) 3407s; run: log: (pid 13825) 2432047s -run: nginx: (pid 30849) 14207s; run: log: (pid 13856) 2432046s -run: node-exporter: (pid 30929) 14206s; run: log: (pid 13877) 2432045s -run: postgres-exporter: (pid 30935) 14206s; run: log: (pid 13931) 2432044s -run: postgresql: (pid 13133) 2432214s; run: log: (pid 13848) 2432046s -run: prometheus: (pid 30807) 14209s; run: log: (pid 13884) 2432045s -run: redis: (pid 30560) 14274s; run: log: (pid 13807) 2432047s -run: redis-exporter: (pid 30946) 14205s; run: log: (pid 13869) 2432045s -run: sidekiq: (pid 30953) 14205s; run: log: (pid 13810) 2432047s -run: unicorn: (pid 30960) 14204s; run: log: (pid 13809) 2432047s +Gitaly executes git operations from gitlab-shell and the GitLab web app, and provides an API to the GitLab web app to get attributes from git (e.g. title, branches, tags, other meta data), and to get blobs (e.g. diffs, commits, files). + +You may also be interested in the [production architecture of GitLab.com](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/). + +### Component diagram + +```mermaid +graph TB + + HTTP[HTTP/HTTPS] -- TCP 80, 443 --> NGINX[NGINX] + SSH -- TCP 22 --> GitLabShell[GitLab Shell] + SMTP[SMTP Gateway] + Geo[GitLab Geo Node] -- TCP 22, 80, 443 --> NGINX + + GitLabShell --TCP 8080 -->Unicorn["Unicorn (GitLab Rails)"] + GitLabShell --> Gitaly + GitLabShell --> Redis + Unicorn --> PgBouncer[PgBouncer] + Unicorn --> Redis + Unicorn --> Gitaly + Redis --> Sidekiq + Sidekiq["Sidekiq (GitLab Rails, ES Indexer)"] --> PgBouncer + GitLabWorkhorse[GitLab Workhorse] --> Unicorn + GitLabWorkhorse --> Redis + GitLabWorkhorse --> Gitaly + Gitaly --> Redis + NGINX --> GitLabWorkhorse + NGINX -- TCP 8090 --> GitLabPages[GitLab Pages] + NGINX --> Grafana[Grafana] + Grafana -- TCP 9090 --> Prometheus[Prometheus] + Prometheus -- TCP 80, 443 --> Unicorn + RedisExporter[Redis Exporter] --> Redis + Prometheus -- TCP 9121 --> RedisExporter + PostgreSQLExporter[PostgreSQL Exporter] --> PostgreSQL + PgBouncerExporter[PgBouncer Exporter] --> PgBouncer + Prometheus -- TCP 9187 --> PostgreSQLExporter + Prometheus -- TCP 9100 --> NodeExporter[Node Exporter] + Prometheus -- TCP 9168 --> GitLabMonito[GitLab Monitor] + Prometheus -- TCP 9127 --> PgBouncerExporter + GitLabMonitor --> PostgreSQL + GitLabMonitor --> GitLabShell + GitLabMonitor --> Sidekiq + PgBouncer --> Consul + PostgreSQL --> Consul + PgBouncer --> PostgreSQL + NGINX --> Registry + Unicorn --> Registry + NGINX --> Mattermost + Mattermost --- Unicorn + Prometheus --> Alertmanager + Migrations --> PostgreSQL + Runner -- TCP 443 --> NGINX + Unicorn -- TCP 9200 --> ElasticSearch + Sidekiq -- TCP 9200 --> ElasticSearch + Sidekiq -- TCP 80, 443 --> Sentry + Unicorn -- TCP 80, 443 --> Sentry + Sidekiq -- UDP 6831 --> Jaeger + Unicorn -- UDP 6831 --> Jaeger + Gitaly -- UDP 6831 --> Jaeger + GitLabShell -- UDP 6831 --> Jaeger + GitLabWorkhorse -- UDP 6831 --> Jaeger + Alertmanager -- TCP 25 --> SMTP + Sidekiq -- TCP 25 --> SMTP + Unicorn -- TCP 25 --> SMTP + Unicorn -- TCP 369 --> LDAP + Sidekiq -- TCP 369 --> LDAP + Unicorn -- TCP 443 --> ObjectStorage["Object Storage"] + Sidekiq -- TCP 443 --> ObjectStorage + GitLabWorkhorse -- TCP 443 --> ObjectStorage + Registry -- TCP 443 --> ObjectStorage + Geo -- TCP 5432 --> PostgreSQL ``` -### Layers +### Component legend + +* ✅ - Installed by default +* ⚙ - Requires additional configuration, or GitLab Managed Apps +* ⤓ - Manual installation required +* ❌ - Not supported or no instructions available + +Component statuses are linked to configuration documentation for each component. + +### Component list + +| Component | Description | [Omnibus GitLab](https://docs.gitlab.com/omnibus/README.html) | [GitLab chart](https://docs.gitlab.com/charts/) | [Minikube Minimal](https://docs.gitlab.com/charts/development/minikube/#deploying-gitlab-with-minimal-settings) | [GitLab.com](https://gitlab.com) | CE/EE | +| --------- | ----------- |:--------------------:|:------------------:|:-----:|:--------:|:--------:| +| [NGINX](#nginx) | Routes requests to appropriate components, terminates SSL | [✅][nginx-omnibus] | [✅][nginx-charts] | [⚙][nginx-charts] | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#service-architecture) | CE & EE | +| [Unicorn (GitLab Rails)](#unicorn) | Handles requests for the web interface and API | [✅][unicorn-omnibus] | [✅][unicorn-charts] | [✅][unicorn-charts] | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#unicorn) | CE & EE | +| [Sidekiq](#sidekiq) | Background jobs processor | [✅][sidekiq-omnibus] | [✅][sidekiq-charts] | [✅](https://docs.gitlab.com/charts/charts/gitlab/sidekiq/index.html) | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#sidekiq) | CE & EE | +| [Gitaly](#gitaly) | Git RPC service for handling all git calls made by GitLab | [✅][gitaly-omnibus] | [✅][gitaly-charts] | [✅][gitaly-charts] | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#service-architecture) | CE & EE | +| [GitLab Workhorse](#gitlab-workhorse) | Smart reverse proxy, handles large HTTP requests | [✅][workhorse-omnibus] | [✅][workhorse-charts] | [✅][workhorse-charts] | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#service-architecture) | CE & EE | +| [GitLab Shell](#gitlab-shell) | Handles `git` over SSH sessions | [✅][shell-omnibus] | [✅][shell-charts] | [✅][shell-charts] | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#service-architecture) | CE & EE | +| [GitLab Pages](#gitlab-pages) | Hosts static websites | [⚙][pages-omnibus] | [❌][pages-charts] | [❌][pages-charts] | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#gitlab-pages) | CE & EE | +| [Registry](#registry) | Container registry, allows pushing and pulling of images | [⚙][registry-omnibus] | [✅][registry-charts] | [✅][registry-charts] | [✅](https://docs.gitlab.com/ee/user/project/container_registry.html#build-and-push-images) | CE & EE | +| [Redis](#redis) | Caching service | [✅][redis-omnibus] | [✅][redis-omnibus] | [✅][redis-charts] | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#service-architecture) | CE & EE | +| [PostgreSQL](#postgresql) | Database | [✅][postgres-omnibus] | [✅][postgres-charts] | [✅][postgres-charts] | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#postgresql) | CE & EE | +| [PgBouncer](#pgbouncer) | Database connection pooling, failover | [⚙][pgbouncer-omnibus] | [❌][pgbouncer-charts] | [❌][pgbouncer-charts] | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#database-architecture) | EE Only | +| [Consul](#consul) | Database node discovery, failover | [⚙][consul-omnibus] | [❌][consul-charts] | [❌][consul-charts] | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#consul) | EE Only | +| [GitLab self-monitoring: Prometheus](#prometheus) | Time-series database, metrics collection, and query service | [✅][prometheus-omnibus] | [✅][prometheus-charts] | [⚙][prometheus-charts] | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#prometheus) | CE & EE | +| [GitLab self-monitoring: Alertmanager](#alertmanager) | Deduplicates, groups, and routes alerts from Prometheus | [✅][alertmanager-omnibus] | [✅][alertmanager-charts] | [⚙][alertmanager-charts] | [✅](https://about.gitlab.com/handbook/engineering/monitoring/) | CE & EE | +| [GitLab self-monitoring: Grafana](#grafana) | Metrics dashboard | [⚙][grafana-omnibus] | [⤓][grafana-charts] | [⤓][grafana-charts] | [✅](https://dashboards.gitlab.com/d/RZmbBr7mk/gitlab-triage?refresh=30s) | CE & EE | +| [GitLab self-monitoring: Sentry](#sentry) | Track errors generated by the GitLab instance | [⤓][sentry-omnibus] | [❌][sentry-charts] | [❌][sentry-charts] | [✅](https://about.gitlab.com/handbook/support/workflows/services/gitlab_com/500_errors.html#searching-sentry) | CE & EE | +| [GitLab self-monitoring: Jaeger](#jaeger) | View traces generated by the GitLab instance | [❌][jaeger-omnibus] | [❌][jaeger-charts] | [❌][jaeger-charts] | [❌](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/4104) | CE & EE | +| [Redis Exporter](#redis-exporter) | Prometheus endpoint with Redis metrics | [✅][redis-exporter-omnibus] | [✅][redis-exporter-charts] | [✅][redis-exporter-charts] | [✅](https://about.gitlab.com/handbook/engineering/monitoring/) | CE & EE | +| [Postgres Exporter](#postgres-exporter) | Prometheus endpoint with PostgreSQL metrics | [✅][postgres-exporter-omnibus] | [✅][postgres-exporter-charts] | [✅][postgres-exporter-charts] | [✅](https://about.gitlab.com/handbook/engineering/monitoring/) | CE & EE | +| [PgBouncer Exporter](#pgbouncer-exporter) | Prometheus endpoint with PgBouncer metrics | [⚙][pgbouncer-exporter-omnibus] | [❌][pgbouncer-exporter-charts] | [❌][pgbouncer-exporter-charts] | [✅](https://about.gitlab.com/handbook/engineering/monitoring/) | CE & EE | +| [GitLab Monitor](#gitlab-monitor) | Generates a variety of GitLab metrics | [✅][gitlab-monitor-omnibus] | [❌][gitab-monitor-charts] | [❌][gitab-monitor-charts] | [✅](https://about.gitlab.com/handbook/engineering/monitoring/) | CE & EE | +| [Node Exporter](#node-exporter) | Prometheus endpoint with system metrics | [✅][node-exporter-omnibus] | [❌][node-exporter-charts] | [❌][node-exporter-charts] | [✅](https://about.gitlab.com/handbook/engineering/monitoring/) | CE & EE | +| [Mattermost](#mattermost) | Open-source Slack alternative | [⚙][mattermost-omnibus] | [⤓][mattermost-charts] | [⤓][mattermost-charts] | [⤓](https://docs.gitlab.com/ee/user/project/integrations/mattermost_slash_commands.html#manual-configuration), [⤓](https://docs.gitlab.com/ee/user/project/integrations/mattermost.html) | CE & EE | +| [Minio](#minio) | Object storage service | [⤓][minio-omnibus] | [✅][minio-charts] | [✅][minio-charts] | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#storage-architecture) | CE & EE | +| [Runner](#gitlab-runner) | Executes GitLab CI jobs | [⤓][runner-omnibus] | [✅][runner-charts] | [⚙][runner-charts] | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#shared-runners) | CE & EE | +| [Database Migrations](#database-migrations) | Database migrations | [✅][database-migrations-omnibus] | [✅]() | [✅][database-migrations-charts] | [✅][database-migrations-charts] | CE & EE | +| [Certificate Management](#certificate-management) | TLS Settings, Let's Encrypt | [✅][certificate-management-omnibus] | [✅][certificate-management-charts] | [⚙][certificate-management-charts] | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#secrets-management) | CE & EE | +| [GitLab Geo Node](#gitlab-geo) | Geographically distributed GitLab nodes | [⚙][geo-omnibus] | [❌][geo-charts] | [❌][geo-charts] | ✅ | EE Only | +| [LDAP Authentication](#ldap-authentication) | Authenticate users against centralized LDAP directory | [⤓][ldap-omnibus] | [⤓][ldap-charts] | [⤓][ldap-charts] | [❌](https://about.gitlab.com/pricing/#gitlab-com) | CE & EE | +| [Outbound email (SMTP)](#outbound-email) | Send email messages to users | [⤓][outbound-email-omnibus] | [⤓][outbound-email-charts] | [⤓][outbound-email-charts] | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#mail-configuration) | CE & EE | +| [Inbound email (SMTP)](#inbound-email) | Receive messages to update issues | [⤓][inbound-email-omnibus] | [⤓][inbound-email-charts] | [⤓][inbound-email-charts] | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#mail-configuration) | CE & EE | +| [ElasticSearch](#elasticsearch) | Improved search within GitLab | [⤓][elasticsearch-omnibus] | [⤓][elasticsearch-charts] | [⤓][elasticsearch-charts] | [❌](https://gitlab.com/groups/gitlab-org/-/epics/153) | EE Only | +| [Sentry integration](#sentry) | Error tracking for deployed apps | [⤓][sentry-integration] | [⤓][sentry-integration] | [⤓][sentry-integration] | [⤓][sentry-integration] | CE & EE | +| [Jaeger integration](#jaeger) | Distributed tracing for deployed apps | [⤓][jaeger-integration] | [⤓][jaeger-integration] | [⤓][jaeger-integration] | [⤓][jaeger-integration] | EE Only | +| [Kubernetes cluster apps](#kubernetes-cluster-apps) | Deploy [Helm](https://docs.helm.sh/), [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/), [Cert-Manager](https://docs.cert-manager.io/en/latest/), [Prometheus](https://prometheus.io/docs/introduction/overview/), a [Runner](https://docs.gitlab.com/runner/), [JupyterHub](http://jupyter.org/), [Knative](https://cloud.google.com/knative) to a cluster | [⤓][managed-k8s-apps] | [⤓][managed-k8s-apps] | [⤓][managed-k8s-apps] | [⤓][managed-k8s-apps] | CE & EE | + +### Component details + +This document is designed to be consumed by systems administrators and GitLab Support Engineers who want to understand more about the internals of GitLab and how they work together. + +When deployed, GitLab should be considered the amalgamation of the below processes. When troubleshooting or debugging, be as specific as possible as to which component you are referencing. That should increase clarity and reduce confusion. + +**Layers** GitLab can be considered to have two layers from a process perspective: @@ -46,129 +159,274 @@ GitLab can be considered to have two layers from a process perspective: - **Processors**: These processes are responsible for actually performing operations and presenting the service. - **Data**: These services store/expose structured data for the GitLab service. -### alertmanager +#### Alertmanager -- Omnibus configuration options +- [Project page](https://github.com/prometheus/alertmanager/blob/master/README.md) +- Configuration: [Omnibus][alertmanager-omnibus], [Charts][alertmanager-charts] - Layer: Monitoring -[Alert manager](https://prometheus.io/docs/alerting/alertmanager/) is a tool provided by prometheus that _"handles alerts sent by client applications such as the Prometheus server. It takes care of deduplicating, grouping, and routing them to the correct receiver integration such as email, PagerDuty, or OpsGenie. It also takes care of silencing and inhibition of alerts."_ You can read more in [issue gitlab-ce#45740](https://gitlab.com/gitlab-org/gitlab-ce/issues/45740) about what we will be alerting on. +[Alert manager](https://prometheus.io/docs/alerting/alertmanager/) is a tool provided by Prometheus that _"handles alerts sent by client applications such as the Prometheus server. It takes care of deduplicating, grouping, and routing them to the correct receiver integration such as email, PagerDuty, or OpsGenie. It also takes care of silencing and inhibition of alerts."_ You can read more in [issue gitlab-ce#45740](https://gitlab.com/gitlab-org/gitlab-ce/issues/45740) about what we will be alerting on. + +#### Certificate management -### gitaly +- Project page: [Omnibus](https://github.com/certbot/certbot/blob/master/README.rst), [Charts](https://github.com/jetstack/cert-manager/blob/master/README.md) +- Configuration: [Omnibus][certificate-management-omnibus], [Charts][certificate-management-charts] +- Layer: Core Service (Processor) + +#### Consul + +- [Project page](https://github.com/hashicorp/consul/blob/master/README.md) +- Configuration: [Omnibus][consul-omnibus], [Charts][consul-charts] +- Layer: Core Service (Data) -- [Omnibus configuration options](https://gitlab.com/gitlab-org/gitaly/tree/master/doc/configuration) +Consul is a tool for service discovery and configuration. Consul is distributed, highly available, and extremely scalable. + +#### Database migrations + +- Configuration: [Omnibus][registry-omnibus], [Charts][registry-charts] - Layer: Core Service (Data) -Gitaly is a service designed by GitLab to remove our need for NFS for Git storage in distributed deployments of GitLab (Think GitLab.com or High Availability Deployments). As of 11.3.0, this service handles all Git level access in GitLab. You can read more about the project [in the project's readme](https://gitlab.com/gitlab-org/gitaly). +#### Elasticsearch + +- [Project page](https://github.com/elastic/elasticsearch/blob/master/README.textile) +- Configuration: [Omnibus][elasticsearch-omnibus], [Charts][elasticsearch-charts] +- Layer: Core Service (Data) -### gitlab-monitor +Elasticsearch is a distributed RESTful search engine built for the cloud. + +#### Gitaly + +- [Project page](https://gitlab.com/gitlab-org/gitaly/blob/master/README.md) +- Configuration: [Omnibus][gitaly-omnibus], [Charts][gitaly-charts] +- Layer: Core Service (Data) + +Gitaly is a service designed by GitLab to remove our need for NFS for Git storage in distributed deployments of GitLab (think GitLab.com or High Availability Deployments). As of 11.3.0, this service handles all Git level access in GitLab. You can read more about the project [in the project's readme](https://gitlab.com/gitlab-org/gitaly). + +#### Gitlab Geo + +- Configuration: [Omnibus][geo-omnibus], [Charts][geo-charts] +- Layer: Core Service (Processor) -- Omnibus configuration options +#### Gitlab Monitor + +- [Project page](https://gitlab.com/gitlab-org/gitlab-monitor) +- Configuration: [Omnibus][gitlab-monitor-omnibus], [Charts][gitlab-monitor-charts] - Layer: Monitoring -GitLab Monitor is a process designed in house that allows us to export metrics about GitLab application internals to prometheus. You can read more [in the project's readme](https://gitlab.com/gitlab-org/gitlab-monitor) +GitLab Monitor is a process designed in house that allows us to export metrics about GitLab application internals to Prometheus. You can read more [in the project's readme](https://gitlab.com/gitlab-org/gitlab-monitor). + +#### Gitlab Pages + +- Configuration: [Omnibus][pages-omnibus], [Charts][pages-charts] +- Layer: Core Service (Processor) + +GitLab Pages is a feature that allows you to publish static websites directly from a repository in GitLab. + +You can use it either for personal or business websites, such as portfolios, documentation, manifestos, and business presentations. You can also attribute any license to your content. + +#### Gitlab Runner + +- [Project page](https://gitlab.com/gitlab-org/gitlab-runner/blob/master/README.md) +- Configuration: [Omnibus][runner-omnibus], [Charts][runner-charts] +- Layer: Core Service (Processor) + +GitLab Runner runs tests and sends the results to GitLab. + +GitLab CI is the open-source continuous integration service included with GitLab that coordinates the testing. The old name of this project was GitLab CI Multi Runner but please use "GitLab Runner" (without CI) from now on. + +#### Gitlab Shell + +- [Project page](https://gitlab.com/gitlab-org/gitlab-shell/blob/master/README.md) +- Configuration: [Omnibus][shell-omnibus], [Charts][shell-charts] +- Layer: Core Service (Processor) + +[GitLab Shell](https://gitlab.com/gitlab-org/gitlab-shell) is a program designed at GitLab to handle ssh-based `git` sessions, and modifies the list of authorized keys. GitLab Shell is not a Unix shell nor a replacement for Bash or Zsh. -### gitlab-workhorse +#### Gitlab Workhorse -- Omnibus configuration options +- [Project page](https://gitlab.com/gitlab-org/gitlab-workhorse/blob/master/README.md) +- Configuration: [Omnibus][gitlab-workhorse-omnibus], [Charts][gitlab-workhorse-charts] - Layer: Core Service (Processor) -[GitLab Workhorse](https://gitlab.com/gitlab-org/gitlab-workhorse) is a program designed at GitLab to help alleviate pressure from unicorn. You can read more about the [historical reasons for developing](https://about.gitlab.com/2016/04/12/a-brief-history-of-gitlab-workhorse/). It's designed to act as a smart reverse proxy to help speed up GitLab as a whole. +[GitLab Workhorse](https://gitlab.com/gitlab-org/gitlab-workhorse) is a program designed at GitLab to help alleviate pressure from Unicorn. You can read more about the [historical reasons for developing](https://about.gitlab.com/2016/04/12/a-brief-history-of-gitlab-workhorse/). It's designed to act as a smart reverse proxy to help speed up GitLab as a whole. -### logrotate +#### Grafana -- [Omnibus configuration options](https://docs.gitlab.com/omnibus/settings/logs.html#logrotate) +- [Project page](https://github.com/grafana/grafana/blob/master/README.md) +- Configuration: [Omnibus][grafana-omnibus], [Charts][grafana-charts] +- Layer: Monitoring + +Grafana is an open source, feature rich metrics dashboard and graph editor for Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB. + +#### Jaeger + +- [Project page](https://github.com/jaegertracing/jaeger/blob/master/README.md) +- Configuration: [Omnibus][jaeger-omnibus], [Charts][jaeger-charts] +- Layer: Monitoring + +Jaeger, inspired by Dapper and OpenZipkin, is a distributed tracing system. It can be used for monitoring microservices-based distributed systems. + +#### Logrotate + +- [Project page](https://github.com/logrotate/logrotate/blob/master/README.md) +- Configuration: [Omnibus](https://docs.gitlab.com/omnibus/settings/logs.html#logrotate) - Layer: Core Service -GitLab is comprised of a large number of services that all log. We started bundling our own logrotate as of 7.4 to make sure we were logging responsibly. This is just a packaged version of the common opensource offering. +GitLab is comprised of a large number of services that all log. We started bundling our own logrotate as of 7.4 to make sure we were logging responsibly. This is just a packaged version of the common open source offering. -### nginx +#### Mattermost -- [Omnibus configuration options](https://docs.gitlab.com/omnibus/settings/nginx.html) +- [Project page](https://github.com/mattermost/mattermost-server/blob/master/README.md) +- Configuration: [Omnibus][mattermost-omnibus], [Charts][mattermost-charts] +- Layer: Core Service (Processor) + +Mattermost is an open source, private cloud, Slack-alternative from https://mattermost.com. + +#### MinIO + +- [Project page](https://github.com/minio/minio/blob/master/README.md) +- Configuration: [Omnibus][minio-omnibus], [Charts][minio-charts] +- Layer: Core Service (Data) + +MinIO is an object storage server released under Apache License v2.0. It is compatible with Amazon S3 cloud storage service. It is best suited for storing unstructured data such as photos, videos, log files, backups and container / VM images. Size of an object can range from a few KBs to a maximum of 5TB. + +#### NGINX + +- Project page: [Omnibus](https://github.com/nginx/nginx), [Charts](https://github.com/kubernetes/ingress-nginx/blob/master/README.md) +- Configuration: [Omnibus][nginx-omnibus], [Charts][nginx-charts] - Layer: Core Service (Processor) Nginx as an ingress port for all HTTP requests and routes them to the approriate sub-systems within GitLab. We are bundling an unmodified version of the popular open source webserver. -### node-exporter +#### Node Exporter -- [Omnibus configuration options](https://docs.gitlab.com/ee/administration/monitoring/prometheus/node_exporter.html) +- [Project page](https://github.com/prometheus/node_exporter/blob/master/README.md) +- Configuration: [Omnibus][node-exporter-omnibus], [Charts][node-exporter-charts] - Layer: Monitoring -[Node Exporter](https://github.com/prometheus/node_exporter) is a Prometheus tool that gives us metrics on the underlying machine. (Think CPU/Disk/Load) It's just a packaged version of the common open source offering from the Prometheus project. +[Node Exporter](https://github.com/prometheus/node_exporter) is a Prometheus tool that gives us metrics on the underlying machine (think CPU/Disk/Load). It's just a packaged version of the common open source offering from the Prometheus project. + +#### PgBouncer + +- [Project page](https://github.com/pgbouncer/pgbouncer/blob/master/README.md) +- Configuration: [Omnibus][pgbouncer-omnibus], [Charts][pgbouncer-charts] +- Layer: Core Service (Data) + +Lightweight connection pooler for PostgreSQL. -### postgres-exporter +#### PgBouncer Exporter -- [Omnibus configuration options](https://docs.gitlab.com/ee/administration/monitoring/prometheus/postgres_exporter.html) +- [Project page](https://github.com/stanhu/pgbouncer_exporter/blob/master/README.md) +- Configuration: [Omnibus][pgbouncer-exporter-omnibus], [Charts][pgbouncer-exporter-charts] - Layer: Monitoring -[Postgres-exporter](https://github.com/wrouesnel/postgres_exporter) is the community provided Prometheus exporter that will deliver data about Postgres to prometheus for use in Grafana Dashboards. +Prometheus exporter for PgBouncer. Exports metrics at 9127/metrics. -### postgresql +#### Postgresql -- [Omnibus configuration options](https://docs.gitlab.com/omnibus/settings/database.html) +- [Project page](https://github.com/postgres/postgres/blob/master/README) +- Configuration: [Omnibus][postgres-omnibus], [Charts][postgres-charts] - Layer: Core Service (Data) GitLab packages the popular Database to provide storage for Application meta data and user information. -### prometheus +#### Postgres Exporter -- [Omnibus configuration options](https://docs.gitlab.com/ee/administration/monitoring/prometheus/) +- [Project page](https://github.com/wrouesnel/postgres_exporter/blob/master/README.md) +- Configuration: [Omnibus][postgres-exporter-omnibus], [Charts][postgres-exporter-charts] +- Layer: Monitoring + +[Postgres-exporter](https://github.com/wrouesnel/postgres_exporter) is the community provided Prometheus exporter that will deliver data about Postgres to Prometheus for use in Grafana Dashboards. + +#### Prometheus + +- [Project page](https://github.com/prometheus/prometheus/blob/master/README.md) +- Configuration: [Omnibus][prometheus-omnibus], [Charts][prometheus-charts] - Layer: Monitoring Prometheus is a time-series tool that helps GitLab administrators expose metrics about the individual processes used to provide GitLab the service. -### redis +#### Redis -- [Omnibus configuration options](https://docs.gitlab.com/omnibus/settings/redis.html) +- [Project page](https://github.com/antirez/redis/blob/unstable/README.md) +- Configuration: [Omnibus][redis-omnibus], [Charts][redis-charts] - Layer: Core Service (Data) Redis is packaged to provide a place to store: - session data - temporary cache information -- background job queues. +- background job queues -### redis-exporter +#### Redis Exporter -- [Omnibus configuration options](https://docs.gitlab.com/ee/administration/monitoring/prometheus/redis_exporter.html) +- [Project page](https://github.com/oliver006/redis_exporter/blob/master/README.md) +- Configuration: [Omnibus][redis-exporter-omnibus], [Charts][redis-exporter-charts] - Layer: Monitoring -[Redis Exporter](https://github.com/oliver006/redis_exporter) is designed to give specific metrics about the Redis process to Prometheus so that we can graph these metrics in Graphana. +[Redis Exporter](https://github.com/oliver006/redis_exporter) is designed to give specific metrics about the Redis process to Prometheus so that we can graph these metrics in Grafana. -### sidekiq +#### Registry -- Omnibus configuration options +- [Project page](https://github.com/docker/distribution/blob/master/README.md) +- Configuration: [Omnibus][registry-omnibus], [Charts][registry-charts] +- Layer: Core Service (Processor) + +The registry is what users use to store their own Docker images. The bundled +registry uses nginx as a load balancer and GitLab as an authentication manager. +Whenever a client requests to pull or push an image from the registry, it will +return a `401` response along with a header detailing where to get an +authentication token, in this case the GitLab instance. The client will then +request a pull or push auth token from GitLab and retry the original request +to the registry. Learn more about [token authentication](https://docs.docker.com/registry/spec/auth/token/). + +An external registry can also be configured to use GitLab as an auth endpoint. + +#### Sentry + +- [Project page](https://github.com/getsentry/sentry/blob/master/README.rst) +- Configuration: [Omnibus][sentry-omnibus], [Charts][sentry-charts] +- Layer: Monitoring + +Sentry fundamentally is a service that helps you monitor and fix crashes in realtime. The server is in Python, but it contains a full API for sending events from any language, in any application. + +#### Sidekiq + +- [Project page](https://github.com/mperham/sidekiq/blob/master/README.md) +- Configuration: [Omnibus][sidekiq-omnibus], [Charts][sidekiq-charts] - Layer: Core Service (Processor) Sidekiq is a Ruby background job processor that pulls jobs from the redis queue and processes them. Background jobs allow GitLab to provide a faster request/response cycle by moving work into the background. -### unicorn +#### Unicorn -- [Omnibus configuration options](https://docs.gitlab.com/omnibus/settings/unicorn.html) +- [Project page](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/README.md) +- Configuration: [Omnibus][unicorn-omnibus], [Charts][unicorn-charts] - Layer: Core Service (Processor) [Unicorn](https://bogomips.org/unicorn/) is a Ruby application server that is used to run the core Rails Application that provides the user facing features in GitLab. Often process output you will see this as `bundle` or `config.ru` depending on the GitLab version. -### Additional Processes +#### LDAP Authentication -### GitLab Pages +- Configuration: [Omnibus][ldap-omnibus], [Charts][ldap-charts] +- Layer: Core Service (Processor) -TODO +#### Outbound Email -### Mattermost +- Configuration: [Omnibus][outbound-email-omnibus], [Charts][outbound-email-charts] +- Layer: Core Service (Processor) -TODO +#### Inbound Email -### Registry +- Configuration: [Omnibus][inbound-email-omnibus], [Charts][inbound-email-charts] +- Layer: Core Service (Processor) -The registry is what users use to store their own Docker images. The bundled -registry uses nginx as a load balancer and GitLab as an authentication manager. -Whenever a client requests to pull or push an image from the registry, it will -return a `401` response along with a header detailing where to get an -authentication token, in this case the GitLab instance. The client will then -request a pull or push auth token from GitLab and retry the original request -to the registry. Learn more about [token authentication](https://docs.docker.com/registry/spec/auth/token/). +#### Kubernetes Cluster Apps -An external registry can also be configured to use GitLab as an auth endpoint. +- Configuration: [Omnibus][managed-k8s-apps], [Charts][managed-k8s-apps] +- Layer: Core Service (Processor) + +GitLab provides [GitLab Managed Apps](https://docs.gitlab.com/ee/user/project/clusters/#installing-applications), a one-click install for various applications which can be added directly to your configured cluster. These applications are needed for Review Apps and deployments when using Auto DevOps. You can install them after you create a cluster. ## GitLab by Request Type @@ -181,10 +439,10 @@ It's important to understand the distinction as some processes are used in both ### GitLab Web HTTP Request Cycle -When making a request to an HTTP Endpoint (Think `/users/sign_in`) the request will take the following path through the GitLab Service: +When making a request to an HTTP Endpoint (think `/users/sign_in`) the request will take the following path through the GitLab Service: -- nginx - Acts as our first line reverse proxy -- gitlab-workhorse - This determines if it needs to go to the Rails application or somewhere else to reduce load on unicorn. +- nginx - Acts as our first line reverse proxy. +- gitlab-workhorse - This determines if it needs to go to the Rails application or somewhere else to reduce load on Unicorn. - unicorn - Since this is a web request, and it needs to access the application it will go to Unicorn. - Postgres/Gitaly/Redis - Depending on the type of request, it may hit these services to store or retrieve data. @@ -200,7 +458,7 @@ TODO ## System Layout -When referring to `~git` in the pictures it means the home directory of the git user which is typically /home/git. +When referring to `~git` in the pictures it means the home directory of the git user which is typically `/home/git`. GitLab is primarily installed within the `/home/git` user home directory as `git` user. Within the home directory is where the gitlabhq server software resides as well as the repositories (though the repository location is configurable). @@ -208,24 +466,6 @@ The bare repositories are located in `/home/git/repositories`. GitLab is a ruby To serve repositories over SSH there's an add-on application called gitlab-shell which is installed in `/home/git/gitlab-shell`. -### Components - -<img src="https://docs.google.com/drawings/d/1fBzAyklyveF-i-2q-OHUIqDkYfjjxC4mq5shwKSZHLs/pub?w=987&h=797"> - -_[edit diagram (for GitLab team members only)](https://docs.google.com/drawings/d/1fBzAyklyveF-i-2q-OHUIqDkYfjjxC4mq5shwKSZHLs/edit)_ - -A typical install of GitLab will be on GNU/Linux. It uses Nginx or Apache as a web front end to proxypass the Unicorn web server. By default, communication between Unicorn and the front end is via a Unix domain socket but forwarding requests via TCP is also supported. The web front end accesses `/home/git/gitlab/public` bypassing the Unicorn server to serve static pages, uploads (e.g. avatar images or attachments), and precompiled assets. GitLab serves web pages and a [GitLab API](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/api) using the Unicorn web server. It uses Sidekiq as a job queue which, in turn, uses redis as a non-persistent database backend for job information, meta data, and incoming jobs. - -The GitLab web app uses MySQL or PostgreSQL for persistent database information (e.g. users, permissions, issues, other meta data). GitLab stores the bare git repositories it serves in `/home/git/repositories` by default. It also keeps default branch and hook information with the bare repository. - -When serving repositories over HTTP/HTTPS GitLab utilizes the GitLab API to resolve authorization and access as well as serving git objects. - -The add-on component gitlab-shell serves repositories over SSH. It manages the SSH keys within `/home/git/.ssh/authorized_keys` which should not be manually edited. gitlab-shell accesses the bare repositories through Gitaly to serve git objects and communicates with redis to submit jobs to Sidekiq for GitLab to process. gitlab-shell queries the GitLab API to determine authorization and access. - -Gitaly executes git operations from gitlab-shell and the GitLab web app, and provides an API to the GitLab web app to get attributes from git (e.g. title, branches, tags, other meta data), and to get blobs (e.g. diffs, commits, files). - -You may also be interested in the [production architecture of GitLab.com](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/). - ### Installation Folder Summary To summarize here's the [directory structure of the `git` user home directory](../install/structure.md). @@ -352,3 +592,71 @@ Note: It is recommended to log into the `git` user using `sudo -i -u git` or `su ## GitLab.com We've also detailed [our architecture of GitLab.com](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/) but this is probably over the top unless you have millions of users. + +[alertmanager-omnibus]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-config-template/gitlab.rb.template +[alertmanager-charts]: https://github.com/helm/charts/tree/master/stable/prometheus +[nginx-omnibus]: https://docs.gitlab.com/omnibus/settings/nginx.html +[nginx-charts]: https://docs.gitlab.com/charts/charts/nginx/index.html +[unicorn-omnibus]: https://docs.gitlab.com/omnibus/settings/unicorn.html +[unicorn-charts]: https://docs.gitlab.com/charts/charts/gitlab/unicorn/index.html +[sidekiq-omnibus]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-config-template/gitlab.rb.template +[sidekiq-charts]: https://docs.gitlab.com/charts/charts/gitlab/sidekiq/index.html +[gitaly-omnibus]: https://docs.gitlab.com/ee/administration/gitaly/ +[gitaly-charts]: https://docs.gitlab.com/charts/charts/gitlab/gitaly/index.html +[workhorse-omnibus]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-config-template/gitlab.rb.template +[workhorse-charts]: https://docs.gitlab.com/charts/charts/gitlab/unicorn/index.html +[shell-omnibus]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-config-template/gitlab.rb.template +[shell-charts]: https://docs.gitlab.com/charts/charts/gitlab/gitlab-shell/index.html +[pages-omnibus]: https://docs.gitlab.com/ee/administration/pages/ +[pages-charts]: https://gitlab.com/charts/gitlab/issues/37 +[registry-omnibus]: https://docs.gitlab.com/ee/administration/container_registry.html#container-registry-domain-configuration +[registry-charts]: https://docs.gitlab.com/charts/charts/registry/index.html +[redis-omnibus]: https://docs.gitlab.com/omnibus/settings/redis.html +[redis-charts]: https://docs.gitlab.com/charts/charts/redis/index.html +[postgres-omnibus]: https://docs.gitlab.com/omnibus/settings/database.html +[postgres-charts]: https://github.com/helm/charts/tree/master/stable/postgresql +[pgbouncer-omnibus]: https://docs.gitlab.com/ee/administration/high_availability/pgbouncer.html +[pgbouncer-charts]: https://docs.gitlab.com/charts/installation/deployment.html#postgresql +[consul-omnibus]: https://docs.gitlab.com/ee/administration/high_availability/consul.html +[consul-charts]: https://docs.gitlab.com/charts/installation/deployment.html#postgresql +[prometheus-omnibus]: https://docs.gitlab.com/ee/administration/monitoring/prometheus/ +[prometheus-charts]: https://github.com/helm/charts/tree/master/stable/prometheus +[grafana-omnibus]: https://docs.gitlab.com/ee/administration/monitoring/performance/grafana_configuration.html +[grafana-charts]: https://github.com/helm/charts/tree/master/stable/grafana +[sentry-omnibus]: https://docs.gitlab.com/omnibus/settings/configuration.html#error-reporting-and-logging-with-sentry +[sentry-charts]: https://gitlab.com/charts/gitlab/issues/1319 +[jaeger-omnibus]: https://gitlab.com/gitlab-org/omnibus-gitlab/issues/4104 +[jaeger-charts]: https://gitlab.com/charts/gitlab/issues/1320 +[redis-exporter-omnibus]: https://docs.gitlab.com/ee/administration/monitoring/prometheus/redis_exporter.html +[redis-exporter-charts]: https://docs.gitlab.com/charts/charts/redis/index.html +[postgres-exporter-omnibus]: https://docs.gitlab.com/ee/administration/monitoring/prometheus/postgres_exporter.html +[postgres-exporter-charts]: https://github.com/helm/charts/tree/master/stable/postgresql +[pgbouncer-exporter-omnibus]: https://docs.gitlab.com/ee/administration/monitoring/prometheus/pgbouncer_exporter.html +[pgbouncer-exporter-charts]: https://docs.gitlab.com/charts/installation/deployment.html#postgresql +[gitlab-monitor-omnibus]: https://docs.gitlab.com/ee/administration/monitoring/prometheus/gitlab_monitor_exporter.html +[gitab-monitor-charts]: https://gitlab.com/charts/gitlab/issues/319 +[node-exporter-omnibus]: https://docs.gitlab.com/ee/administration/monitoring/prometheus/node_exporter.html +[node-exporter-charts]: https://gitlab.com/charts/gitlab/issues/1332 +[mattermost-omnibus]: https://docs.gitlab.com/omnibus/gitlab-mattermost/ +[mattermost-charts]: https://docs.mattermost.com/install/install-mmte-helm-gitlab-helm.html +[minio-omnibus]: https://min.io/download +[minio-charts]: https://docs.gitlab.com/charts/charts/minio/index.html +[runner-omnibus]: https://docs.gitlab.com/runner/ +[runner-charts]: https://docs.gitlab.com/runner/install/kubernetes.html +[database-migrations-omnibus]: https://docs.gitlab.com/omnibus/settings/database.html#disabling-automatic-database-migration +[database-migrations-charts]: https://docs.gitlab.com/charts/charts/gitlab/migrations/index.html +[certificate-management-omnibus]: https://docs.gitlab.com/omnibus/settings/ssl.html +[certificate-management-charts]: https://docs.gitlab.com/charts/installation/tls.html +[geo-omnibus]: https://docs.gitlab.com/ee/administration/geo/replication/index.html#setup-instructions +[geo-charts]: https://gitlab.com/charts/gitlab/issues/8 +[ldap-omnibus]: https://docs.gitlab.com/ee/administration/auth/ldap.html +[ldap-charts]: https://docs.gitlab.com/charts/charts/globals.html#ldap +[outbound-email-omnibus]: https://docs.gitlab.com/omnibus/settings/smtp.html +[outbound-email-charts]: https://docs.gitlab.com/charts/installation/command-line-options.html#outgoing-email-configuration +[inbound-email-omnibus]: https://docs.gitlab.com/ee/administration/incoming_email.html +[inbound-email-charts]: https://docs.gitlab.com/charts/installation/command-line-options.html#incoming-email-configuration +[elasticsearch-omnibus]: https://docs.gitlab.com/ee/integration/elasticsearch.html +[elasticsearch-charts]: https://docs.gitlab.com/ee/integration/elasticsearch.html +[sentry-integration]: https://docs.gitlab.com/ee/user/project/operations/error_tracking.html +[jaeger-integration]: https://docs.gitlab.com/ee/user/project/operations/tracing.html +[managed-k8s-apps]: https://docs.gitlab.com/ee/user/project/clusters/#installing-applications diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md index 52a4bb27817..4bf8401c0e8 100644 --- a/doc/development/documentation/index.md +++ b/doc/development/documentation/index.md @@ -52,6 +52,57 @@ Adhere to the [Documentation Style Guide](styleguide.md). If a style standard is See the [Structure](styleguide.md#structure) section of the [Documentation Style Guide](styleguide.md). +## Single codebase + +We currently maintain two sets of docs: one in the +[gitlab-ce](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc) repo and +one in [gitlab-ee](https://gitlab.com/gitlab-org/gitlab-ee/tree/master/doc). +They are similar, and most pages are identical, but they are different repositories. +With the single codebase effort, we want to make those two sets identical, so when the +time comes to have only one codebase, we'll be ready. + +Here are some links to get you up to speed with the current effort: + +- [CE/EE codebases blueprint](https://about.gitlab.com/handbook/engineering/infrastructure/blueprint/ce-ee-codebases/) +- [CE/EE codebases merge design](https://about.gitlab.com/handbook/engineering/infrastructure/design/merge-ce-ee-codebases/) +- [Single docs codebase epic](https://gitlab.com/groups/gitlab-org/-/epics/199) +- [Issue board of related issues](https://gitlab.com/groups/gitlab-org/-/boards/981090?&label_name[]=Documentation&label_name[]=single%20codebase) +- [Related merge requests](https://gitlab.com/groups/gitlab-org/-/merge_requests?scope=all&utf8=%E2%9C%93&state=all&label_name[]=Documentation&label_name[]=single%20codebase) +- [Visualize the existing diffs](https://leipert-projects.gitlab.io/is-gitlab-pretty-yet/diff/?search=%5Edoc) + +### CE first + +After a given documentation path is aligned across CE and EE, all merge requests +affecting that path must be submitted to CE, regardless of the content it has. +This means that for EE-only features which are being added only to the EE codebase, +you have to submit a separate merge request in CE that contains the docs. +This might seem like a duplicate effort, but it's for the short term. +A list of the already aligned docs can be found in +[the epic description](https://gitlab.com/groups/gitlab-org/-/epics/199#ee-specific-lines-check). + +Since the docs will be combined, it's crucial to add the relevant +[product badges](styleguide.md#product-badges) for all EE documentation, so that +we can discern which features belong to which tier. + +### EE specific lines check + +There's a special test in place +([`ee_specific_check.rb`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/scripts/ee_specific_check/ee_specific_check.rb)), +which, among others, checks and prevents creating/editing new files and directories +in EE under `doc/`. + +We have a long list of documentation paths that are either whitelisted or not. +Paths in the whitelist (not commented out) will not be subject to the test, +which means you are allowed to create/change docs content in EE for the time +being. The goal is to not have any doc whitelisted. + +At the time of this writing, the only items left to be aligned are the API docs: + +- `doc/api/*` ([issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/60045) / [merge request](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/27491)) + +Eventually, once all docs are aligned, we'll remove any doc reference from that +script, so it catches everything. + ## Changing document location Changing a document's location requires specific steps to be followed to ensure that @@ -438,6 +489,10 @@ Currently, the following tests are in place: As CE is merged into EE once a day, it's important to avoid merge conflicts. Submitting an EE-equivalent merge request cherry-picking all commits from CE to EE is essential to avoid them. +1. [`ee-files-location-check`/`ee-specific-lines-check`](#ee-specific-lines-check) (runs on EE only): + This test ensures that no new files/directories are created/changed in EE. + All docs should be submitted in CE instead, regardless the tier they are on. + This is for the [single codebase](#single-codebase) effort. 1. In a full pipeline, tests for [`/help`](#gitlab-help-tests). ### Linting diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md index 9853b38b8e9..bc472bb5b0a 100644 --- a/doc/development/ee_features.md +++ b/doc/development/ee_features.md @@ -446,6 +446,28 @@ The disadvantage of this: port `render_if_exists` to CE. - If we have typos in the partial name, it would be silently ignored. + +##### Caveats + +The `render_if_exists` view path argument must be relative to `app/views/` and `ee/app/views`. +Resolving an EE template path that is relative to the CE view path will not work. + +```haml +- # app/views/projects/index.html.haml + += render_if_exists 'button' # Will not render `ee/app/views/projects/_button` and will quietly fail += render_if_exists 'projects/button' # Will render `ee/app/views/projects/_button` +``` + +You should not explicitly set render options like `partial` or provide a `locals` hash. +The first argument should be a path string and the second can be a hash replacing `locals`. + +```ruby +render partial: 'projects/button', locals: { project: project } +# becomes +render_if_exists 'projects/button', project: project +``` + #### Using `render_ce` For `render` and `render_if_exists`, they search for the EE partial first, @@ -888,7 +910,7 @@ information on managing page-specific javascript within EE. ### script tag #### Child Component only used in EE -To seperate Vue template differences we should [async import the components](https://vuejs.org/v2/guide/components-dynamic-async.html#Async-Components). +To separate Vue template differences we should [async import the components](https://vuejs.org/v2/guide/components-dynamic-async.html#Async-Components). Doing this allows for us to load the correct component in EE whilst in CE we can load a empty component that renders nothing. This code **should** diff --git a/doc/development/fe_guide/graphql.md b/doc/development/fe_guide/graphql.md index 5f6123b5f9b..8e06aa5d173 100644 --- a/doc/development/fe_guide/graphql.md +++ b/doc/development/fe_guide/graphql.md @@ -43,9 +43,9 @@ new Vue({ Read more about [Vue Apollo][vue-apollo] in the [Vue Apollo documentation][vue-apollo-docs]. -### Local state with `apollo-link-state` +### Local state with Apollo -It is possible to use our Apollo setup with [apollo-link-state][apollo-link-state] by passing +It is possible to manage an application state with Apollo by passing in a resolvers object when creating the default client. The default state can be set by writing to the cache after setting up the default client. @@ -76,6 +76,8 @@ const apolloProvider = new VueApollo({ }); ``` +Read more about local state management with Apollo in the [Vue Apollo documentation](https://vue-apollo.netlify.com/guide/local-state.html#local-state). + ### Testing With [Vue test utils][vue-test-utils] it is easy to quickly test components that @@ -92,6 +94,8 @@ it('tests apollo component', () => { }); ``` +Another possible way is testing queries with mocked GraphQL schema. Read more about this way in [Vue Apollo testing documentation](https://vue-apollo.netlify.com/guide/testing.html#tests-with-mocked-graqhql-schema) + ## Usage outside of Vue It is also possible to use GraphQL outside of Vue by directly importing diff --git a/doc/development/fe_guide/vuex.md b/doc/development/fe_guide/vuex.md index 7d52cac5f7e..bf248b7f8af 100644 --- a/doc/development/fe_guide/vuex.md +++ b/doc/development/fe_guide/vuex.md @@ -186,7 +186,7 @@ Remember that actions only describe that something happened, they don't describe state.users.push(user); }, [types.REQUEST_ADD_USER_ERROR](state, error) { - state.isAddingUser = true; + state.isAddingUser = false; state.errorAddingUser = error; }, }; @@ -231,7 +231,7 @@ The store should be included in the main component of your application: ```javascript // app.vue - import store from 'store'; // it will include the index.js file + import store from './store'; // it will include the index.js file export default { name: 'application', diff --git a/doc/development/feature_flags.md b/doc/development/feature_flags.md index 3271f9a7fb3..82b09cc0224 100644 --- a/doc/development/feature_flags.md +++ b/doc/development/feature_flags.md @@ -108,11 +108,11 @@ so we make sure behavior under feature flag doesn't go untested in some non-spec contexts. Whenever a feature flag is present, make sure to test _both_ states of the -feature flag. You can stub a feature flag as follows: +feature flag. -```ruby -stub_feature_flags(my_feature_flag: false) -``` +See the +[testing guide](testing_guide/best_practices.html#feature-flags-in-tests) +for information and examples on how to stub feature flags in tests. ## Enabling a feature flag (in development) diff --git a/doc/development/gitlab_architecture_diagram.png b/doc/development/gitlab_architecture_diagram.png Binary files differdeleted file mode 100644 index 90e27d5462a..00000000000 --- a/doc/development/gitlab_architecture_diagram.png +++ /dev/null diff --git a/doc/development/go_guide/index.md b/doc/development/go_guide/index.md index b9dc3797e5b..cf78b792ffd 100644 --- a/doc/development/go_guide/index.md +++ b/doc/development/go_guide/index.md @@ -40,7 +40,7 @@ of possible security breaches in our code: - SQL injections Remember to run -[SAST](https://docs.gitlab.com/ee/user/project/merge_requests/sast.html) +[SAST](https://docs.gitlab.com/ee/user/application_security/sast/index) **[ULTIMATE]** on your project (or at least the [gosec analyzer](https://gitlab.com/gitlab-org/security-products/analyzers/gosec)), and to follow our [Security @@ -96,7 +96,7 @@ dependency should be argued in the merge request, as per our [Approval Guidelines](../code_review.md#approval-guidelines). Both [License Management](https://docs.gitlab.com/ee/user/project/merge_requests/license_management.html) **[ULTIMATE]** and [Dependency -Scanning](https://docs.gitlab.com/ee/user/project/merge_requests/dependency_scanning.html) +Scanning](https://docs.gitlab.com/ee/user/application_security/dependency_scanning/index) **[ULTIMATE]** should be activated on all projects to ensure new dependencies security status and license compatibility. diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md index ac04a21b37a..faea8d5982f 100644 --- a/doc/development/i18n/proofreader.md +++ b/doc/development/i18n/proofreader.md @@ -58,6 +58,7 @@ are very appreciative of the work done by translators and proofreaders! - Japanese - Yamana Tokiuji - [GitLab](https://gitlab.com/tokiuji), [Crowdin](https://crowdin.com/profile/yamana) - Hiroyuki Sato - [GitLab](https://gitlab.com/hiroponz), [Crowdin](https://crowdin.com/profile/hiroponz) + - Tomo Dote - [Gitlab](https://gitlab.com/fu7mu4), [Crowdin](https://crowdin.com/profile/fu7mu4) - Korean - Chang-Ho Cha - [GitLab](https://gitlab.com/changho-cha), [Crowdin](https://crowdin.com/profile/zzazang) - Ji Hun Oh - [GitLab](https://gitlab.com/Baw-Appie), [Crowdin](https://crowdin.com/profile/BawAppie) diff --git a/doc/development/rolling_out_changes_using_feature_flags.md b/doc/development/rolling_out_changes_using_feature_flags.md index 8d35a4ecee2..8e4e07c4319 100644 --- a/doc/development/rolling_out_changes_using_feature_flags.md +++ b/doc/development/rolling_out_changes_using_feature_flags.md @@ -65,15 +65,14 @@ the worst case scenario, which we should optimise for, our total cost is now 20. If we had used a feature flag, things would have been very different. We don't need to revert a release, and because feature flags are disabled by default we don't need to revert and pick any Git commits. In fact, all we have to do is -disable the feature, and _maybe_ perform some cleanup. Let's say that the cost -of this is 1. In this case, our best case cost is 11: 10 to build the feature, -and 1 to add the feature flag. The worst case cost is now 12: 10 to build the -feature, 1 to add the feature flag, and 1 to disable it. +disable the feature, and in the worst case, perform cleanup. Let's say that +the cost of this is 2. In this case, our best case cost is 11: 10 to build the +feature, and 1 to add the feature flag. The worst case cost is now 13: 10 to +build the feature, 1 to add the feature flag, and 2 to disable and clean up. Here we can see that in the best case scenario the work necessary is only a tiny bit more compared to not using a feature flag. Meanwhile, the process of -reverting our changes has been made significantly cheaper, to the point of being -trivial. +reverting our changes has been made significantly and reliably cheaper. In other words, feature flags do not slow down the development process. Instead, they speed up the process as managing incidents now becomes _much_ easier. Once diff --git a/doc/development/session.md b/doc/development/session.md new file mode 100644 index 00000000000..9edce3dbda0 --- /dev/null +++ b/doc/development/session.md @@ -0,0 +1,65 @@ +# Accessing session data + +Session data in GitLab is stored in Redis and can be accessed in a variety of ways. + +During a web request, for example: + +- Rails provides access to the session from within controllers through [`ActionDispatch::Session`](https://guides.rubyonrails.org/action_controller_overview.html#session). +- Outside of controllers, it is possible to access the session through `Gitlab::Session`. + +Outside of a web request it is still possible to access sessions stored in Redis. For example: + +- Session IDs and contents can be [looked up directly in Redis](#redis). +- Data about the UserAgent associated with the session can be accessed through `ActiveSession`. + +When storing values in a session it is best to: + +- Use simple primitives and avoid storing objects to avoid marshaling complications. +- Clean up after unneeded variables to keep memory usage in Redis down. + +## Gitlab::Session + +Sometimes you might want to persist data in the session instead of another store like the database. `Gitlab::Session` lets you access this without passing the session around extensively. For example, you could access it from within a policy without having to pass the session through to each place permissions are checked from. + +The session has a hash-like interface, just like when using it from a controller. There is also `NamespacedSessionStore` for storing key-value data in a hash. + +```ruby +# Lookup a value stored in the current session +Gitlab::Session.current[:my_feature] + +# Modify the current session stored in redis +Gitlab::Session.current[:my_feature] = value + +# Store key-value data namespaced under a key +Gitlab::NamespacedSessionStore.new(:my_feature)[some_key] = value + +# Set the session for a block of code, such as for tests +Gitlab::Session.with_session(my_feature: value) do + # Code that uses Session.current[:my_feature] +end +``` + +## Redis + +Session data can be accessed directly through Redis. This can let you check up on a browser session when debugging. + +```ruby +# Get a list of sessions +session_ids = Gitlab::Redis::SharedState.with do |redis| + redis.smembers("#{Gitlab::Redis::SharedState::USER_SESSIONS_LOOKUP_NAMESPACE}:#{user.id}") +end + +# Retrieve a specific session +session_data = Gitlab::Redis::SharedState.with { |redis| redis.get("#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}") } +Marshal.load(session_data) +``` + +## Getting device information with ActiveSession + +The [**Active Sessions** page on a user's profile](../user/profile/active_sessions.md) displays information about the device used to access each session. The methods used there to list sessions can also be useful for development. + +```ruby +# Get list of sessions for a given user +# Includes session_id and data from the UserAgent +ActiveSession.list(user) +``` diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md index e41148360f2..63ec9755462 100644 --- a/doc/development/testing_guide/best_practices.md +++ b/doc/development/testing_guide/best_practices.md @@ -240,6 +240,36 @@ it 'is overdue' do end ``` +### Feature flags in tests + +All feature flags are stubbed to be enabled by default in our Ruby-based +tests. + +To disable a feature flag in a test, use the `stub_feature_flags` +helper. For example, to globally disable the `ci_live_trace` feature +flag in a test: + +```ruby +stub_feature_flags(ci_live_trace: false) + +Feature.enabled?(:ci_live_trace) # => false +``` + +If you wish to set up a test where a feature flag is disabled for some +actors and not others, you can specify this in options passed to the +helper. For example, to disable the `ci_live_trace` feature flag for a +specifc project: + +```ruby +project1, project2 = build_list(:project, 2) + +# Feature will only be disabled for project1 +stub_feature_flags(ci_live_trace: { enabled: false, thing: project1 }) + +Feature.enabled?(:ci_live_trace, project1) # => false +Feature.enabled?(:ci_live_trace, project2) # => true +``` + ### Pristine test environments The code exercised by a single GitLab test may access and modify many items of diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md index f58a8dcbcdc..9bd99e80357 100644 --- a/doc/development/testing_guide/frontend_testing.md +++ b/doc/development/testing_guide/frontend_testing.md @@ -187,6 +187,7 @@ export default function doSomething() { visitUrl('/foo/bar'); } ``` + ```js // my_module_spec.js import doSomething from '~/my_module'; @@ -213,7 +214,187 @@ Further documentation on the babel rewire pluign API can be found on #### Waiting in tests -If you cannot avoid using [`setTimeout`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout) in tests, please use the [Jasmine mock clock](https://jasmine.github.io/api/2.9/Clock.html). +Sometimes a test needs to wait for something to happen in the application before it continues. +Avoid using [`setTimeout`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout) +because it makes the reason for waiting unclear and if passed a time larger than zero it will slow down our test suite. +Instead use one of the following approaches. + +##### Promises and Ajax calls + +Register handler functions to wait for the `Promise` to be resolved. + +```javascript +const askTheServer = () => { + return axios + .get('/endpoint') + .then(response => { + // do something + }) + .catch(error => { + // do something else + }); +}; +``` + +**in Jest:** + +```javascript +it('waits for an Ajax call', () => { + return askTheServer().then(() => { + expect(something).toBe('done'); + }); +}); +``` + +**in Karma:** + +```javascript +it('waits for an Ajax call', done => { + askTheServer() + .then(() => { + expect(something).toBe('done'); + }) + .then(done) + .catch(done.fail); +}); +``` + +If you are not able to register handlers to the `Promise`—for example because it is executed in a synchronous Vue life +cycle hook—you can flush all pending `Promise`s: + +**in Jest:** + +```javascript +it('waits for an Ajax call', () => { + synchronousFunction(); + jest.runAllTicks(); + + expect(something).toBe('done'); +}); +``` + +**in Karma:** + +You are out of luck. The following only works sometimes and may lead to flaky failures: + +```javascript +it('waits for an Ajax call', done => { + synchronousFunction(); + + // create a new Promise and hope that it resolves after the rest + Promise.resolve() + .then(() => { + expect(something).toBe('done'); + }) + .then(done) + .catch(done.fail); +}); +``` + +##### Vue rendering + +To wait until a Vue component is re-rendered, use either of the equivalent +[`Vue.nextTick()`](https://vuejs.org/v2/api/#Vue-nextTick) or `vm.$nextTick()`. + +**in Jest:** + +```javascript +it('renders something', () => { + wrapper.setProps({ value: 'new value' }); + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.text()).toBe('new value'); + }); +}); +``` + +**in Karma:** + +```javascript +it('renders something', done => { + wrapper.setProps({ value: 'new value' }); + + wrapper.vm + .$nextTick() + .then(() => { + expect(wrapper.text()).toBe('new value'); + }) + .then(done) + .catch(done.fail); +}); +``` + +##### `setTimeout()` / `setInterval()` in application + +If the application itself is waiting for some time, mock await the waiting. In Jest this is already +[done by default](https://gitlab.com/gitlab-org/gitlab-ce/blob/a2128edfee799e49a8732bfa235e2c5e14949c68/jest.config.js#L47) +(see also [Jest Timer Mocks](https://jestjs.io/docs/en/timer-mocks)). In Karma you can use the +[Jasmine mock clock](https://jasmine.github.io/api/2.9/Clock.html). + +```javascript +const doSomethingLater = () => { + setTimeout(() => { + // do something + }, 4000); +}; +``` + +**in Jest:** + +```javascript +it('does something', () => { + doSomethingLater(); + jest.runAllTimers(); + + expect(something).toBe('done'); +}); +``` + +**in Karma:** + +```javascript +it('does something', () => { + jasmine.clock().install(); + + doSomethingLater(); + jasmine.clock().tick(4000); + + expect(something).toBe('done'); + jasmine.clock().uninstall(); +}); +``` + +##### Events + +If the application triggers an event that you need to wait for in your test, register an event handler which contains +the assertions: + +```javascript +it('waits for an event', done => { + eventHub.$once('someEvent', eventHandler); + + someFunction(); + + function eventHandler() { + expect(something).toBe('done'); + done(); + } +}); +``` + +In Jest you can also use a `Promise` for this: + +```javascript +it('waits for an event', () => { + const eventTriggered = new Promise(resolve => eventHub.$once('someEvent', resolve)); + + someFunction(); + + return eventTriggered.then(() => { + expect(something).toBe('done'); + }); +}); +``` #### Migrating flaky Karma tests to Jest diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md index 7ad33f05f77..f34793c11f4 100644 --- a/doc/development/testing_guide/review_apps.md +++ b/doc/development/testing_guide/review_apps.md @@ -78,17 +78,27 @@ subgraph CNG-mirror pipeline **Additional notes:** -- The Kubernetes cluster is connected to the `gitlab-{ce,ee}` projects using - [GitLab's Kubernetes integration][gitlab-k8s-integration]. This basically - allows to have a link to the Review App directly from the merge request - widget. -- If the Review App deployment fails, you can simply retry it (there's no need - to run the [`review-stop`][gitlab-ci-yml] job first). +- If the `review-deploy` job keep failing (note that we already retry it twice), + please post a message in the `#quality` channel and/or create a ~Quality ~bug + issue with a link to your merge request. Note that the deployment failure can + reveal an actual problem introduced in your merge request (i.e. this isn't + necessarily a transient failure)! +- If the `review-qa-smoke` job keep failing (note that we already retry it twice), + please check the job's logs: you could discover an actual problem introduced in + your merge request. You can also download the artifacts to see screenshots of + the page at the time the failures occurred. If you don't find the cause of the + failure or if it seems unrelated to your change, please post a message in the + `#quality` channel and/or create a ~Quality ~bug issue with a link to your + merge request. - The manual [`review-stop`][gitlab-ci-yml] in the `test` stage can be used to stop a Review App manually, and is also started by GitLab once a merge request's branch is deleted after being merged. - Review Apps are cleaned up regularly via a pipeline schedule that runs the [`schedule:review-cleanup`][gitlab-ci-yml] job. +- The Kubernetes cluster is connected to the `gitlab-{ce,ee}` projects using + [GitLab's Kubernetes integration][gitlab-k8s-integration]. This basically + allows to have a link to the Review App directly from the merge request + widget. ## QA runs diff --git a/doc/development/ux_guide/img/animation-autoscroll.gif b/doc/development/ux_guide/img/animation-autoscroll.gif Binary files differdeleted file mode 100644 index 155b0234c64..00000000000 --- a/doc/development/ux_guide/img/animation-autoscroll.gif +++ /dev/null diff --git a/doc/development/ux_guide/img/animation-dropdown.gif b/doc/development/ux_guide/img/animation-dropdown.gif Binary files differdeleted file mode 100644 index c9b31d26165..00000000000 --- a/doc/development/ux_guide/img/animation-dropdown.gif +++ /dev/null diff --git a/doc/development/ux_guide/img/animation-hover.gif b/doc/development/ux_guide/img/animation-hover.gif Binary files differdeleted file mode 100644 index 37ad9c76828..00000000000 --- a/doc/development/ux_guide/img/animation-hover.gif +++ /dev/null diff --git a/doc/development/ux_guide/img/animation-quickupdate.gif b/doc/development/ux_guide/img/animation-quickupdate.gif Binary files differdeleted file mode 100644 index 8db70bc3d24..00000000000 --- a/doc/development/ux_guide/img/animation-quickupdate.gif +++ /dev/null diff --git a/doc/development/ux_guide/img/animation-reorder.gif b/doc/development/ux_guide/img/animation-reorder.gif Binary files differdeleted file mode 100644 index ccdeb3d396f..00000000000 --- a/doc/development/ux_guide/img/animation-reorder.gif +++ /dev/null diff --git a/doc/development/ux_guide/img/button-close--active.png b/doc/development/ux_guide/img/button-close--active.png Binary files differdeleted file mode 100644 index 97a5301fb91..00000000000 --- a/doc/development/ux_guide/img/button-close--active.png +++ /dev/null diff --git a/doc/development/ux_guide/img/button-close--hover.png b/doc/development/ux_guide/img/button-close--hover.png Binary files differdeleted file mode 100644 index 6b8fdf5695b..00000000000 --- a/doc/development/ux_guide/img/button-close--hover.png +++ /dev/null diff --git a/doc/development/ux_guide/img/button-close--resting.png b/doc/development/ux_guide/img/button-close--resting.png Binary files differdeleted file mode 100644 index 5679b51687c..00000000000 --- a/doc/development/ux_guide/img/button-close--resting.png +++ /dev/null diff --git a/doc/development/ux_guide/img/button-danger--active.png b/doc/development/ux_guide/img/button-danger--active.png Binary files differdeleted file mode 100644 index 6a9aab0fcc2..00000000000 --- a/doc/development/ux_guide/img/button-danger--active.png +++ /dev/null diff --git a/doc/development/ux_guide/img/button-danger--hover.png b/doc/development/ux_guide/img/button-danger--hover.png Binary files differdeleted file mode 100644 index 13e21c28779..00000000000 --- a/doc/development/ux_guide/img/button-danger--hover.png +++ /dev/null diff --git a/doc/development/ux_guide/img/button-danger--resting.png b/doc/development/ux_guide/img/button-danger--resting.png Binary files differdeleted file mode 100644 index 0ff192bc463..00000000000 --- a/doc/development/ux_guide/img/button-danger--resting.png +++ /dev/null diff --git a/doc/development/ux_guide/img/button-info--active.png b/doc/development/ux_guide/img/button-info--active.png Binary files differdeleted file mode 100644 index 12ecdc72a31..00000000000 --- a/doc/development/ux_guide/img/button-info--active.png +++ /dev/null diff --git a/doc/development/ux_guide/img/button-info--hover.png b/doc/development/ux_guide/img/button-info--hover.png Binary files differdeleted file mode 100644 index 3bf93bf2b32..00000000000 --- a/doc/development/ux_guide/img/button-info--hover.png +++ /dev/null diff --git a/doc/development/ux_guide/img/button-info--resting.png b/doc/development/ux_guide/img/button-info--resting.png Binary files differdeleted file mode 100644 index a37a37033bf..00000000000 --- a/doc/development/ux_guide/img/button-info--resting.png +++ /dev/null diff --git a/doc/development/ux_guide/img/button-primary.png b/doc/development/ux_guide/img/button-primary.png Binary files differdeleted file mode 100644 index eda5ed84aec..00000000000 --- a/doc/development/ux_guide/img/button-primary.png +++ /dev/null diff --git a/doc/development/ux_guide/img/button-secondary.png b/doc/development/ux_guide/img/button-secondary.png Binary files differdeleted file mode 100644 index 26d4e8cf43d..00000000000 --- a/doc/development/ux_guide/img/button-secondary.png +++ /dev/null diff --git a/doc/development/ux_guide/img/button-spam--active.png b/doc/development/ux_guide/img/button-spam--active.png Binary files differdeleted file mode 100644 index a9e115f49c1..00000000000 --- a/doc/development/ux_guide/img/button-spam--active.png +++ /dev/null diff --git a/doc/development/ux_guide/img/button-spam--hover.png b/doc/development/ux_guide/img/button-spam--hover.png Binary files differdeleted file mode 100644 index 3b2c16430a6..00000000000 --- a/doc/development/ux_guide/img/button-spam--hover.png +++ /dev/null diff --git a/doc/development/ux_guide/img/button-spam--resting.png b/doc/development/ux_guide/img/button-spam--resting.png Binary files differdeleted file mode 100644 index 4f9f18ca68a..00000000000 --- a/doc/development/ux_guide/img/button-spam--resting.png +++ /dev/null diff --git a/doc/development/ux_guide/img/button-success--active.png b/doc/development/ux_guide/img/button-success--active.png Binary files differdeleted file mode 100644 index b99f6f5e70e..00000000000 --- a/doc/development/ux_guide/img/button-success--active.png +++ /dev/null diff --git a/doc/development/ux_guide/img/button-success--hover.png b/doc/development/ux_guide/img/button-success--hover.png Binary files differdeleted file mode 100644 index 0d0a61c679a..00000000000 --- a/doc/development/ux_guide/img/button-success--hover.png +++ /dev/null diff --git a/doc/development/ux_guide/img/button-success--resting.png b/doc/development/ux_guide/img/button-success--resting.png Binary files differdeleted file mode 100644 index 53b955c650a..00000000000 --- a/doc/development/ux_guide/img/button-success--resting.png +++ /dev/null diff --git a/doc/development/ux_guide/img/button-success-secondary--active.png b/doc/development/ux_guide/img/button-success-secondary--active.png Binary files differdeleted file mode 100644 index 333a91f2217..00000000000 --- a/doc/development/ux_guide/img/button-success-secondary--active.png +++ /dev/null diff --git a/doc/development/ux_guide/img/button-success-secondary--hover.png b/doc/development/ux_guide/img/button-success-secondary--hover.png Binary files differdeleted file mode 100644 index 0cce59212e3..00000000000 --- a/doc/development/ux_guide/img/button-success-secondary--hover.png +++ /dev/null diff --git a/doc/development/ux_guide/img/button-success-secondary--resting.png b/doc/development/ux_guide/img/button-success-secondary--resting.png Binary files differdeleted file mode 100644 index 2779a4949f8..00000000000 --- a/doc/development/ux_guide/img/button-success-secondary--resting.png +++ /dev/null diff --git a/doc/development/ux_guide/img/button-warning--active.png b/doc/development/ux_guide/img/button-warning--active.png Binary files differdeleted file mode 100644 index f5760cd7c12..00000000000 --- a/doc/development/ux_guide/img/button-warning--active.png +++ /dev/null diff --git a/doc/development/ux_guide/img/button-warning--hover.png b/doc/development/ux_guide/img/button-warning--hover.png Binary files differdeleted file mode 100644 index a1f4c5cbcc6..00000000000 --- a/doc/development/ux_guide/img/button-warning--hover.png +++ /dev/null diff --git a/doc/development/ux_guide/img/button-warning--resting.png b/doc/development/ux_guide/img/button-warning--resting.png Binary files differdeleted file mode 100644 index 3d62fed5930..00000000000 --- a/doc/development/ux_guide/img/button-warning--resting.png +++ /dev/null diff --git a/doc/development/ux_guide/img/color-blue.png b/doc/development/ux_guide/img/color-blue.png Binary files differdeleted file mode 100644 index 77c1a2cab31..00000000000 --- a/doc/development/ux_guide/img/color-blue.png +++ /dev/null diff --git a/doc/development/ux_guide/img/color-green.png b/doc/development/ux_guide/img/color-green.png Binary files differdeleted file mode 100644 index 51600584c96..00000000000 --- a/doc/development/ux_guide/img/color-green.png +++ /dev/null diff --git a/doc/development/ux_guide/img/color-grey.png b/doc/development/ux_guide/img/color-grey.png Binary files differdeleted file mode 100644 index f0f0b9d80bb..00000000000 --- a/doc/development/ux_guide/img/color-grey.png +++ /dev/null diff --git a/doc/development/ux_guide/img/color-orange.png b/doc/development/ux_guide/img/color-orange.png Binary files differdeleted file mode 100644 index f16435c0a64..00000000000 --- a/doc/development/ux_guide/img/color-orange.png +++ /dev/null diff --git a/doc/development/ux_guide/img/color-red.png b/doc/development/ux_guide/img/color-red.png Binary files differdeleted file mode 100644 index 5008e75da78..00000000000 --- a/doc/development/ux_guide/img/color-red.png +++ /dev/null diff --git a/doc/development/ux_guide/img/color-textprimary.png b/doc/development/ux_guide/img/color-textprimary.png Binary files differdeleted file mode 100644 index 90f2821f0cf..00000000000 --- a/doc/development/ux_guide/img/color-textprimary.png +++ /dev/null diff --git a/doc/development/ux_guide/img/color-textsecondary.png b/doc/development/ux_guide/img/color-textsecondary.png Binary files differdeleted file mode 100644 index 61cb8a13c45..00000000000 --- a/doc/development/ux_guide/img/color-textsecondary.png +++ /dev/null diff --git a/doc/development/ux_guide/img/components-alerts.png b/doc/development/ux_guide/img/components-alerts.png Binary files differdeleted file mode 100644 index 66a43ac69e1..00000000000 --- a/doc/development/ux_guide/img/components-alerts.png +++ /dev/null diff --git a/doc/development/ux_guide/img/components-anchorlinks.png b/doc/development/ux_guide/img/components-anchorlinks.png Binary files differdeleted file mode 100644 index bd8d30f5905..00000000000 --- a/doc/development/ux_guide/img/components-anchorlinks.png +++ /dev/null diff --git a/doc/development/ux_guide/img/components-contentblock.png b/doc/development/ux_guide/img/components-contentblock.png Binary files differdeleted file mode 100644 index 58d87729701..00000000000 --- a/doc/development/ux_guide/img/components-contentblock.png +++ /dev/null diff --git a/doc/development/ux_guide/img/components-counts.png b/doc/development/ux_guide/img/components-counts.png Binary files differdeleted file mode 100644 index 19280e988a0..00000000000 --- a/doc/development/ux_guide/img/components-counts.png +++ /dev/null diff --git a/doc/development/ux_guide/img/components-coverblock.png b/doc/development/ux_guide/img/components-coverblock.png Binary files differdeleted file mode 100644 index 61160de5613..00000000000 --- a/doc/development/ux_guide/img/components-coverblock.png +++ /dev/null diff --git a/doc/development/ux_guide/img/components-dateexact.png b/doc/development/ux_guide/img/components-dateexact.png Binary files differdeleted file mode 100644 index cc1fb8216bf..00000000000 --- a/doc/development/ux_guide/img/components-dateexact.png +++ /dev/null diff --git a/doc/development/ux_guide/img/components-daterelative.png b/doc/development/ux_guide/img/components-daterelative.png Binary files differdeleted file mode 100644 index 4954dfb51b3..00000000000 --- a/doc/development/ux_guide/img/components-daterelative.png +++ /dev/null diff --git a/doc/development/ux_guide/img/components-dropdown.png b/doc/development/ux_guide/img/components-dropdown.png Binary files differdeleted file mode 100644 index 7f9a701c089..00000000000 --- a/doc/development/ux_guide/img/components-dropdown.png +++ /dev/null diff --git a/doc/development/ux_guide/img/components-fileholder.png b/doc/development/ux_guide/img/components-fileholder.png Binary files differdeleted file mode 100644 index 5bf8565346a..00000000000 --- a/doc/development/ux_guide/img/components-fileholder.png +++ /dev/null diff --git a/doc/development/ux_guide/img/components-horizontalform.png b/doc/development/ux_guide/img/components-horizontalform.png Binary files differdeleted file mode 100644 index e6cbc69d20a..00000000000 --- a/doc/development/ux_guide/img/components-horizontalform.png +++ /dev/null diff --git a/doc/development/ux_guide/img/components-listinsidepanel.png b/doc/development/ux_guide/img/components-listinsidepanel.png Binary files differdeleted file mode 100644 index 6b773a19954..00000000000 --- a/doc/development/ux_guide/img/components-listinsidepanel.png +++ /dev/null diff --git a/doc/development/ux_guide/img/components-listwithavatar.png b/doc/development/ux_guide/img/components-listwithavatar.png Binary files differdeleted file mode 100644 index f6db575433c..00000000000 --- a/doc/development/ux_guide/img/components-listwithavatar.png +++ /dev/null diff --git a/doc/development/ux_guide/img/components-listwithhover.png b/doc/development/ux_guide/img/components-listwithhover.png Binary files differdeleted file mode 100644 index 0826848ff34..00000000000 --- a/doc/development/ux_guide/img/components-listwithhover.png +++ /dev/null diff --git a/doc/development/ux_guide/img/components-panels.png b/doc/development/ux_guide/img/components-panels.png Binary files differdeleted file mode 100644 index c1391ca07e5..00000000000 --- a/doc/development/ux_guide/img/components-panels.png +++ /dev/null diff --git a/doc/development/ux_guide/img/components-referencehover.png b/doc/development/ux_guide/img/components-referencehover.png Binary files differdeleted file mode 100644 index af5405d3e0b..00000000000 --- a/doc/development/ux_guide/img/components-referencehover.png +++ /dev/null diff --git a/doc/development/ux_guide/img/components-referenceissues.png b/doc/development/ux_guide/img/components-referenceissues.png Binary files differdeleted file mode 100644 index 4e175dc169d..00000000000 --- a/doc/development/ux_guide/img/components-referenceissues.png +++ /dev/null diff --git a/doc/development/ux_guide/img/components-referencelabels.png b/doc/development/ux_guide/img/components-referencelabels.png Binary files differdeleted file mode 100644 index 29a985bbaa0..00000000000 --- a/doc/development/ux_guide/img/components-referencelabels.png +++ /dev/null diff --git a/doc/development/ux_guide/img/components-referencemilestone.png b/doc/development/ux_guide/img/components-referencemilestone.png Binary files differdeleted file mode 100644 index 47c76a9d60f..00000000000 --- a/doc/development/ux_guide/img/components-referencemilestone.png +++ /dev/null diff --git a/doc/development/ux_guide/img/components-referencemrs.png b/doc/development/ux_guide/img/components-referencemrs.png Binary files differdeleted file mode 100644 index 9a5032a1516..00000000000 --- a/doc/development/ux_guide/img/components-referencemrs.png +++ /dev/null diff --git a/doc/development/ux_guide/img/components-referencepeople.png b/doc/development/ux_guide/img/components-referencepeople.png Binary files differdeleted file mode 100644 index f9ef11be853..00000000000 --- a/doc/development/ux_guide/img/components-referencepeople.png +++ /dev/null diff --git a/doc/development/ux_guide/img/components-rowcontentblock.png b/doc/development/ux_guide/img/components-rowcontentblock.png Binary files differdeleted file mode 100644 index c66a50f9564..00000000000 --- a/doc/development/ux_guide/img/components-rowcontentblock.png +++ /dev/null diff --git a/doc/development/ux_guide/img/components-searchbox.png b/doc/development/ux_guide/img/components-searchbox.png Binary files differdeleted file mode 100644 index 5c19024bfb0..00000000000 --- a/doc/development/ux_guide/img/components-searchbox.png +++ /dev/null diff --git a/doc/development/ux_guide/img/components-searchboxscoped.png b/doc/development/ux_guide/img/components-searchboxscoped.png Binary files differdeleted file mode 100644 index d4a35977658..00000000000 --- a/doc/development/ux_guide/img/components-searchboxscoped.png +++ /dev/null diff --git a/doc/development/ux_guide/img/components-simplelist.png b/doc/development/ux_guide/img/components-simplelist.png Binary files differdeleted file mode 100644 index 8d11c674e84..00000000000 --- a/doc/development/ux_guide/img/components-simplelist.png +++ /dev/null diff --git a/doc/development/ux_guide/img/components-table.png b/doc/development/ux_guide/img/components-table.png Binary files differdeleted file mode 100644 index cedc55758a9..00000000000 --- a/doc/development/ux_guide/img/components-table.png +++ /dev/null diff --git a/doc/development/ux_guide/img/components-verticalform.png b/doc/development/ux_guide/img/components-verticalform.png Binary files differdeleted file mode 100644 index 489ae6f862f..00000000000 --- a/doc/development/ux_guide/img/components-verticalform.png +++ /dev/null diff --git a/doc/development/ux_guide/img/cursors-default.png b/doc/development/ux_guide/img/cursors-default.png Binary files differdeleted file mode 100644 index c188ec4e351..00000000000 --- a/doc/development/ux_guide/img/cursors-default.png +++ /dev/null diff --git a/doc/development/ux_guide/img/cursors-ibeam.png b/doc/development/ux_guide/img/cursors-ibeam.png Binary files differdeleted file mode 100644 index 86f28639982..00000000000 --- a/doc/development/ux_guide/img/cursors-ibeam.png +++ /dev/null diff --git a/doc/development/ux_guide/img/cursors-move.png b/doc/development/ux_guide/img/cursors-move.png Binary files differdeleted file mode 100644 index a9c610eaa88..00000000000 --- a/doc/development/ux_guide/img/cursors-move.png +++ /dev/null diff --git a/doc/development/ux_guide/img/cursors-panclosed.png b/doc/development/ux_guide/img/cursors-panclosed.png Binary files differdeleted file mode 100644 index 6d247a765ac..00000000000 --- a/doc/development/ux_guide/img/cursors-panclosed.png +++ /dev/null diff --git a/doc/development/ux_guide/img/cursors-panopened.png b/doc/development/ux_guide/img/cursors-panopened.png Binary files differdeleted file mode 100644 index 76f2eeda831..00000000000 --- a/doc/development/ux_guide/img/cursors-panopened.png +++ /dev/null diff --git a/doc/development/ux_guide/img/cursors-pointer.png b/doc/development/ux_guide/img/cursors-pointer.png Binary files differdeleted file mode 100644 index d86dd955fa7..00000000000 --- a/doc/development/ux_guide/img/cursors-pointer.png +++ /dev/null diff --git a/doc/development/ux_guide/img/features-contextualnav.png b/doc/development/ux_guide/img/features-contextualnav.png Binary files differdeleted file mode 100644 index aa816776fad..00000000000 --- a/doc/development/ux_guide/img/features-contextualnav.png +++ /dev/null diff --git a/doc/development/ux_guide/img/features-emptystates.png b/doc/development/ux_guide/img/features-emptystates.png Binary files differdeleted file mode 100644 index 50f31f5e523..00000000000 --- a/doc/development/ux_guide/img/features-emptystates.png +++ /dev/null diff --git a/doc/development/ux_guide/img/features-filters.png b/doc/development/ux_guide/img/features-filters.png Binary files differdeleted file mode 100644 index 41db76db938..00000000000 --- a/doc/development/ux_guide/img/features-filters.png +++ /dev/null diff --git a/doc/development/ux_guide/img/features-globalnav.png b/doc/development/ux_guide/img/features-globalnav.png Binary files differdeleted file mode 100644 index 73294d1b524..00000000000 --- a/doc/development/ux_guide/img/features-globalnav.png +++ /dev/null diff --git a/doc/development/ux_guide/img/harry-robison.png b/doc/development/ux_guide/img/harry-robison.png Binary files differdeleted file mode 100644 index 702a8b02262..00000000000 --- a/doc/development/ux_guide/img/harry-robison.png +++ /dev/null diff --git a/doc/development/ux_guide/img/icon-add.png b/doc/development/ux_guide/img/icon-add.png Binary files differdeleted file mode 100644 index f66525cc1b4..00000000000 --- a/doc/development/ux_guide/img/icon-add.png +++ /dev/null diff --git a/doc/development/ux_guide/img/icon-close.png b/doc/development/ux_guide/img/icon-close.png Binary files differdeleted file mode 100644 index af6c30ebe6a..00000000000 --- a/doc/development/ux_guide/img/icon-close.png +++ /dev/null diff --git a/doc/development/ux_guide/img/icon-edit.png b/doc/development/ux_guide/img/icon-edit.png Binary files differdeleted file mode 100644 index b9649f4aeec..00000000000 --- a/doc/development/ux_guide/img/icon-edit.png +++ /dev/null diff --git a/doc/development/ux_guide/img/icon-notification.png b/doc/development/ux_guide/img/icon-notification.png Binary files differdeleted file mode 100644 index 5cf8f8ab59a..00000000000 --- a/doc/development/ux_guide/img/icon-notification.png +++ /dev/null diff --git a/doc/development/ux_guide/img/icon-rss.png b/doc/development/ux_guide/img/icon-rss.png Binary files differdeleted file mode 100644 index 7e2987a2656..00000000000 --- a/doc/development/ux_guide/img/icon-rss.png +++ /dev/null diff --git a/doc/development/ux_guide/img/icon-spec.png b/doc/development/ux_guide/img/icon-spec.png Binary files differdeleted file mode 100644 index 5bb85c5be98..00000000000 --- a/doc/development/ux_guide/img/icon-spec.png +++ /dev/null diff --git a/doc/development/ux_guide/img/icon-subscribe.png b/doc/development/ux_guide/img/icon-subscribe.png Binary files differdeleted file mode 100644 index 7e2f5e6a1c6..00000000000 --- a/doc/development/ux_guide/img/icon-subscribe.png +++ /dev/null diff --git a/doc/development/ux_guide/img/icon-trash.png b/doc/development/ux_guide/img/icon-trash.png Binary files differdeleted file mode 100644 index bc46638fb2e..00000000000 --- a/doc/development/ux_guide/img/icon-trash.png +++ /dev/null diff --git a/doc/development/ux_guide/img/illustration-size-large-horizontal.png b/doc/development/ux_guide/img/illustration-size-large-horizontal.png Binary files differdeleted file mode 100644 index 8aa835adccc..00000000000 --- a/doc/development/ux_guide/img/illustration-size-large-horizontal.png +++ /dev/null diff --git a/doc/development/ux_guide/img/illustration-size-large-vertical.png b/doc/development/ux_guide/img/illustration-size-large-vertical.png Binary files differdeleted file mode 100644 index 813b6a065e5..00000000000 --- a/doc/development/ux_guide/img/illustration-size-large-vertical.png +++ /dev/null diff --git a/doc/development/ux_guide/img/illustration-size-medium.png b/doc/development/ux_guide/img/illustration-size-medium.png Binary files differdeleted file mode 100644 index 55cfe1dcb91..00000000000 --- a/doc/development/ux_guide/img/illustration-size-medium.png +++ /dev/null diff --git a/doc/development/ux_guide/img/illustration-size-small.png b/doc/development/ux_guide/img/illustration-size-small.png Binary files differdeleted file mode 100644 index 0124f58f48e..00000000000 --- a/doc/development/ux_guide/img/illustration-size-small.png +++ /dev/null diff --git a/doc/development/ux_guide/img/illustrations-border-radius.png b/doc/development/ux_guide/img/illustrations-border-radius.png Binary files differdeleted file mode 100644 index 4e2fef5c7f5..00000000000 --- a/doc/development/ux_guide/img/illustrations-border-radius.png +++ /dev/null diff --git a/doc/development/ux_guide/img/illustrations-caps-do.png b/doc/development/ux_guide/img/illustrations-caps-do.png Binary files differdeleted file mode 100644 index f1030769b94..00000000000 --- a/doc/development/ux_guide/img/illustrations-caps-do.png +++ /dev/null diff --git a/doc/development/ux_guide/img/illustrations-caps-don't.png b/doc/development/ux_guide/img/illustrations-caps-don't.png Binary files differdeleted file mode 100644 index ab7abcaaf6f..00000000000 --- a/doc/development/ux_guide/img/illustrations-caps-don't.png +++ /dev/null diff --git a/doc/development/ux_guide/img/illustrations-color-grey.png b/doc/development/ux_guide/img/illustrations-color-grey.png Binary files differdeleted file mode 100644 index 63855026c2b..00000000000 --- a/doc/development/ux_guide/img/illustrations-color-grey.png +++ /dev/null diff --git a/doc/development/ux_guide/img/illustrations-color-orange.png b/doc/development/ux_guide/img/illustrations-color-orange.png Binary files differdeleted file mode 100644 index 96765c8c28c..00000000000 --- a/doc/development/ux_guide/img/illustrations-color-orange.png +++ /dev/null diff --git a/doc/development/ux_guide/img/illustrations-color-purple.png b/doc/development/ux_guide/img/illustrations-color-purple.png Binary files differdeleted file mode 100644 index 745d2c853ba..00000000000 --- a/doc/development/ux_guide/img/illustrations-color-purple.png +++ /dev/null diff --git a/doc/development/ux_guide/img/illustrations-geometric.png b/doc/development/ux_guide/img/illustrations-geometric.png Binary files differdeleted file mode 100644 index 33f05547bac..00000000000 --- a/doc/development/ux_guide/img/illustrations-geometric.png +++ /dev/null diff --git a/doc/development/ux_guide/img/illustrations-palette-oragne.png b/doc/development/ux_guide/img/illustrations-palette-oragne.png Binary files differdeleted file mode 100644 index 15f35912646..00000000000 --- a/doc/development/ux_guide/img/illustrations-palette-oragne.png +++ /dev/null diff --git a/doc/development/ux_guide/img/illustrations-palette-purple.png b/doc/development/ux_guide/img/illustrations-palette-purple.png Binary files differdeleted file mode 100644 index e0f5839705e..00000000000 --- a/doc/development/ux_guide/img/illustrations-palette-purple.png +++ /dev/null diff --git a/doc/development/ux_guide/img/james-mackey.png b/doc/development/ux_guide/img/james-mackey.png Binary files differdeleted file mode 100644 index f51a45c437b..00000000000 --- a/doc/development/ux_guide/img/james-mackey.png +++ /dev/null diff --git a/doc/development/ux_guide/img/karolina-plaskaty.png b/doc/development/ux_guide/img/karolina-plaskaty.png Binary files differdeleted file mode 100644 index d1c9528dd5a..00000000000 --- a/doc/development/ux_guide/img/karolina-plaskaty.png +++ /dev/null diff --git a/doc/development/ux_guide/img/matthieu-poirier.png b/doc/development/ux_guide/img/matthieu-poirier.png Binary files differdeleted file mode 100644 index 0ecc2d670d6..00000000000 --- a/doc/development/ux_guide/img/matthieu-poirier.png +++ /dev/null diff --git a/doc/development/ux_guide/img/modals-general-confimation-dialog.png b/doc/development/ux_guide/img/modals-general-confimation-dialog.png Binary files differdeleted file mode 100644 index 4ea0ea10ca7..00000000000 --- a/doc/development/ux_guide/img/modals-general-confimation-dialog.png +++ /dev/null diff --git a/doc/development/ux_guide/img/modals-layout-for-modals.png b/doc/development/ux_guide/img/modals-layout-for-modals.png Binary files differdeleted file mode 100644 index c481edd8250..00000000000 --- a/doc/development/ux_guide/img/modals-layout-for-modals.png +++ /dev/null diff --git a/doc/development/ux_guide/img/modals-special-confimation-dialog.png b/doc/development/ux_guide/img/modals-special-confimation-dialog.png Binary files differdeleted file mode 100644 index d966010158b..00000000000 --- a/doc/development/ux_guide/img/modals-special-confimation-dialog.png +++ /dev/null diff --git a/doc/development/ux_guide/img/modals-three-buttons.png b/doc/development/ux_guide/img/modals-three-buttons.png Binary files differdeleted file mode 100644 index 157d1b650bf..00000000000 --- a/doc/development/ux_guide/img/modals-three-buttons.png +++ /dev/null diff --git a/doc/development/ux_guide/img/monospacefont-sample.png b/doc/development/ux_guide/img/monospacefont-sample.png Binary files differdeleted file mode 100644 index 1cd290b713c..00000000000 --- a/doc/development/ux_guide/img/monospacefont-sample.png +++ /dev/null diff --git a/doc/development/ux_guide/img/nazim-ramesh.png b/doc/development/ux_guide/img/nazim-ramesh.png Binary files differdeleted file mode 100644 index dad2b37010b..00000000000 --- a/doc/development/ux_guide/img/nazim-ramesh.png +++ /dev/null diff --git a/doc/development/ux_guide/img/popover-placement-above.png b/doc/development/ux_guide/img/popover-placement-above.png Binary files differdeleted file mode 100644 index 84c9c878ec2..00000000000 --- a/doc/development/ux_guide/img/popover-placement-above.png +++ /dev/null diff --git a/doc/development/ux_guide/img/popover-placement-below.png b/doc/development/ux_guide/img/popover-placement-below.png Binary files differdeleted file mode 100644 index f6f18199ab6..00000000000 --- a/doc/development/ux_guide/img/popover-placement-below.png +++ /dev/null diff --git a/doc/development/ux_guide/img/skeleton-loading.gif b/doc/development/ux_guide/img/skeleton-loading.gif Binary files differdeleted file mode 100644 index 5877139171d..00000000000 --- a/doc/development/ux_guide/img/skeleton-loading.gif +++ /dev/null diff --git a/doc/development/ux_guide/img/sourcesanspro-sample.png b/doc/development/ux_guide/img/sourcesanspro-sample.png Binary files differdeleted file mode 100644 index f7ecf0c7c66..00000000000 --- a/doc/development/ux_guide/img/sourcesanspro-sample.png +++ /dev/null diff --git a/doc/development/ux_guide/img/steven-lyons.png b/doc/development/ux_guide/img/steven-lyons.png Binary files differdeleted file mode 100644 index 2efe1d0b168..00000000000 --- a/doc/development/ux_guide/img/steven-lyons.png +++ /dev/null diff --git a/doc/development/ux_guide/img/surfaces-contentitemtitle.png b/doc/development/ux_guide/img/surfaces-contentitemtitle.png Binary files differdeleted file mode 100644 index f6cd212ecfd..00000000000 --- a/doc/development/ux_guide/img/surfaces-contentitemtitle.png +++ /dev/null diff --git a/doc/development/ux_guide/img/surfaces-header.png b/doc/development/ux_guide/img/surfaces-header.png Binary files differdeleted file mode 100644 index ba616388003..00000000000 --- a/doc/development/ux_guide/img/surfaces-header.png +++ /dev/null diff --git a/doc/development/ux_guide/img/surfaces-systeminformationblock.png b/doc/development/ux_guide/img/surfaces-systeminformationblock.png Binary files differdeleted file mode 100644 index f3313add2b8..00000000000 --- a/doc/development/ux_guide/img/surfaces-systeminformationblock.png +++ /dev/null diff --git a/doc/development/ux_guide/img/surfaces-ux.png b/doc/development/ux_guide/img/surfaces-ux.png Binary files differdeleted file mode 100644 index eaa7f70c0c7..00000000000 --- a/doc/development/ux_guide/img/surfaces-ux.png +++ /dev/null diff --git a/doc/development/ux_guide/img/tooltip-placement.png b/doc/development/ux_guide/img/tooltip-placement.png Binary files differdeleted file mode 100644 index da49c192878..00000000000 --- a/doc/development/ux_guide/img/tooltip-placement.png +++ /dev/null diff --git a/doc/development/ux_guide/img/tooltip-usage.png b/doc/development/ux_guide/img/tooltip-usage.png Binary files differdeleted file mode 100644 index 4f5884c4b48..00000000000 --- a/doc/development/ux_guide/img/tooltip-usage.png +++ /dev/null diff --git a/doc/gitlab-basics/README.md b/doc/gitlab-basics/README.md index aa008d6f768..28ba20bec09 100644 --- a/doc/gitlab-basics/README.md +++ b/doc/gitlab-basics/README.md @@ -27,9 +27,11 @@ The following are guides to basic GitLab functionality: ## Git basics -If you're unfamiliar with the command line, these resources will help: +If you're familiar with Git on the command line, you can interact with your GitLab projects just as you would with any other Git repository. + +These resources will help get further acclimated to working on the command line. -- [Command line basics](command-line-commands.md), for those unfamiliar with the command line interface. - [Start using Git on the command line](start-using-git.md), for some simple Git commands. +- [Command line basics](command-line-commands.md), to create and edit files using the command line. More Git resources are available at GitLab's [Git documentation](../topics/git/index.md). diff --git a/doc/gitlab-basics/command-line-commands.md b/doc/gitlab-basics/command-line-commands.md index a0111be0767..1cf883679d7 100644 --- a/doc/gitlab-basics/command-line-commands.md +++ b/doc/gitlab-basics/command-line-commands.md @@ -13,10 +13,12 @@ button (you'll have to paste it on your shell in the next step). ![Copy the HTTPS or SSH](img/project_clone_url.png) -## On the command line +## Working with project files on the command line This section has examples of some basic shell commands that you might find useful. For more information, search the web for _bash commands_. +Alternatively, you can edit files using your choice of editor (IDE) or the GitLab user interface. + ### Clone your project Go to your computer's shell and type the following command with your SSH or HTTPS URL: diff --git a/doc/gitlab-basics/img/new_issue_button.png b/doc/gitlab-basics/img/new_issue_button.png Binary files differdeleted file mode 100644 index 3b113471f0c..00000000000 --- a/doc/gitlab-basics/img/new_issue_button.png +++ /dev/null diff --git a/doc/gitlab-basics/img/new_issue_page.png b/doc/gitlab-basics/img/new_issue_page.png Binary files differdeleted file mode 100644 index ce3e60df276..00000000000 --- a/doc/gitlab-basics/img/new_issue_page.png +++ /dev/null diff --git a/doc/gitlab-basics/img/public_file_link.png b/doc/gitlab-basics/img/public_file_link.png Binary files differdeleted file mode 100644 index f60df6807f4..00000000000 --- a/doc/gitlab-basics/img/public_file_link.png +++ /dev/null diff --git a/doc/gitlab-basics/start-using-git.md b/doc/gitlab-basics/start-using-git.md index e30afdf8a40..b3c5d32f2f5 100644 --- a/doc/gitlab-basics/start-using-git.md +++ b/doc/gitlab-basics/start-using-git.md @@ -71,6 +71,18 @@ git config --global --list Start using Git via the command line with the most basic commands as described below. +## Initialize a local directory for Git version control + +If you have an existing local directory that you want to *initialize* for version control, use the `init` command to instruct Git to begin tracking the directory: + +```bash +git init +``` + +This creates a `.git` directory that contains the Git configuration files. + +Once the directory has been initialized, you can [add a remote repository](#add-a-remote-repository) and [send changes to GitLab.com](#send-changes-to-gitlabcom). View the instructions on [Create a project](../gitlab-basics/create-project.html#push-to-create-a-new-project) to create a new project on GitLab with your changes. + ### Clone a repository To start working locally on an existing remote repository, @@ -140,6 +152,16 @@ To view your remote repositories, type: git remote -v ``` +### Add a remote repository + +To add a link to a remote repository: + +```bash +git remote add SOURCE-NAME REPOSITORY-PATH +``` + +You'll use this source name every time you [push changes to GitLab.com](#send-changes-to-gitlabcom), so use something easy to remember and type. + ### Create a branch To create a branch, type the following (spaces won't be recognized in the branch name, so you will need to use a hyphen or underscore): @@ -193,7 +215,7 @@ git commit -m "COMMENT TO DESCRIBE THE INTENTION OF THE COMMIT" NOTE: **Note:** The `.` character typically means _all_ in Git. -### Send changes to gitlab.com +### Send changes to GitLab.com To push all local commits to the remote repository: diff --git a/doc/install/aws/img/add_tags.png b/doc/install/aws/img/add_tags.png Binary files differdeleted file mode 100644 index 3572cd5daa1..00000000000 --- a/doc/install/aws/img/add_tags.png +++ /dev/null diff --git a/doc/install/aws/img/create_route_table.png b/doc/install/aws/img/create_route_table.png Binary files differdeleted file mode 100644 index ea72c57257e..00000000000 --- a/doc/install/aws/img/create_route_table.png +++ /dev/null diff --git a/doc/install/azure/img/azure-vm-management-settings-network-interfaces.png b/doc/install/azure/img/azure-vm-management-settings-network-interfaces.png Binary files differdeleted file mode 100644 index 4ff10718059..00000000000 --- a/doc/install/azure/img/azure-vm-management-settings-network-interfaces.png +++ /dev/null diff --git a/doc/install/azure/img/azure-vm-management.png b/doc/install/azure/img/azure-vm-management.png Binary files differdeleted file mode 100644 index a0e0067258c..00000000000 --- a/doc/install/azure/img/azure-vm-management.png +++ /dev/null diff --git a/doc/install/installation.md b/doc/install/installation.md index 60a8ffacd76..6b24dc8abdc 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -99,7 +99,20 @@ sudo apt-get install -y git-core git --version ``` -Is the system packaged Git too old? Remove it and compile from source. +Starting with GitLab 12.0, Git is required to be compiled with `libpcre2`. +Find out if that's the case: + +```sh +ldd /usr/local/bin/git | grep pcre2 +``` + +The output should be similar to: + +``` +libpcre2-8.so.0 => /usr/lib/libpcre2-8.so.0 (0x00007f08461c3000) +``` + +Is the system packaged Git too old, or not compiled with pcre2? Remove it and compile from source: ```sh # Remove packaged Git @@ -108,12 +121,21 @@ sudo apt-get remove git-core # Install dependencies sudo apt-get install -y libcurl4-openssl-dev libexpat1-dev gettext libz-dev libssl-dev build-essential +# Download and compile pcre2 from source +curl --silent --show-error --location https://ftp.pcre.org/pub/pcre/pcre2-10.33.tar.gz --output pcre2.tar.gz +tar -xzf pcre2.tar.gz +cd pcre2-10.33 +chmod +x configure +./configure --prefix=/usr --enable-jit +make +make install + # Download and compile from source cd /tmp curl --remote-name --location --progress https://www.kernel.org/pub/software/scm/git/git-2.21.0.tar.gz -echo '85eca51c7404da75e353eba587f87fea9481ba41e162206a6f70ad8118147bee' git-2.21.0.tar.gz' | shasum -a256 -c - && tar -xzf git-2.21.0.tar.gz +echo '85eca51c7404da75e353eba587f87fea9481ba41e162206a6f70ad8118147bee git-2.21.0.tar.gz' | shasum -a256 -c - && tar -xzf git-2.21.0.tar.gz cd git-2.21.0/ -./configure +./configure --with-libpcre make prefix=/usr/local all # Install into /usr/local/bin @@ -163,9 +185,9 @@ Download Ruby and compile it: ```sh mkdir /tmp/ruby && cd /tmp/ruby -curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.5/ruby-2.5.3.tar.gz -echo 'f919a9fbcdb7abecd887157b49833663c5c15fda ruby-2.5.3.tar.gz' | shasum -c - && tar xzf ruby-2.5.3.tar.gz -cd ruby-2.5.3 +curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.6/ruby-2.6.3.tar.gz +echo '2347ed6ca5490a104ebd5684d2b9b5eefa6cd33c ruby-2.6.3.tar.gz' | shasum -c - && tar xzf ruby-2.6.3.tar.gz +cd ruby-2.6.3 ./configure --disable-install-rdoc make @@ -434,7 +456,8 @@ sudo -u git -H editor config/resque.yml ``` CAUTION: **Caution:** -Make sure to edit both `gitlab.yml` and `unicorn.rb` to match your setup. +Make sure to edit both `gitlab.yml` and `unicorn.rb` to match your setup. +If you want to use Puma web server, see [Using Puma](#using-puma) for the additional steps. NOTE: **Note:** If you want to use HTTPS, see [Using HTTPS](#using-https) for the additional steps. @@ -448,6 +471,18 @@ sudo -u git cp config/database.yml.postgresql config/database.yml # MySQL only: sudo -u git cp config/database.yml.mysql config/database.yml +# PostgreSQL only: +# Remove host, username, and password lines from config/database.yml. +# Once modified, the `production` settings will be as follows: +# +# production: +# adapter: postgresql +# encoding: unicode +# database: gitlabhq_production +# pool: 10 +# +sudo -u git -H editor config/database.yml + # MySQL and remote PostgreSQL only: # Update username/password in config/database.yml. # You only need to adapt the production settings (first part). @@ -565,6 +600,18 @@ sudo -u git -H editor config.toml For more information about configuring Gitaly see [doc/administration/gitaly](../administration/gitaly). +### Start Gitaly + +Gitaly must be running for the next section. + +```sh +gitlab_path=/home/git/gitlab +gitaly_path=/home/git/gitaly + +sudo -u git -H $gitlab_path/bin/daemon_with_pidfile $gitlab_path/tmp/pids/gitaly.pid \ + $gitaly_path/gitaly $gitaly_path/config.toml >> $gitlab_path/log/gitaly.log 2>&1 & +``` + ### Initialize Database and Activate Advanced Features ```sh @@ -579,10 +626,10 @@ sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production force=yes ``` NOTE: **Note:** -You can set the Administrator/root password and e-mail by supplying them in environmental variables, `GITLAB_ROOT_PASSWORD` and `GITLAB_ROOT_EMAIL` respectively, as seen below. If you don't set the password (and it is set to the default one), wait to expose GitLab to the public internet until the installation is done and you've logged into the server the first time. During the first login, you'll be forced to change the default password. +You can set the Administrator/root password and e-mail by supplying them in environmental variables, `GITLAB_ROOT_PASSWORD` and `GITLAB_ROOT_EMAIL` respectively, as seen below. If you don't set the password (and it is set to the default one), wait to expose GitLab to the public internet until the installation is done and you've logged into the server the first time. During the first login, you'll be forced to change the default password. An Enterprise Edition license may also be installed at this time by supplying a full path in the `GITLAB_LICENSE_FILE` environment variable. ```sh -sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production GITLAB_ROOT_PASSWORD=yourpassword GITLAB_ROOT_EMAIL=youremail +sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production GITLAB_ROOT_PASSWORD=yourpassword GITLAB_ROOT_EMAIL=youremail GITLAB_LICENSE_FILE="/path/to/license" ``` ### Secure secrets.yml @@ -640,6 +687,12 @@ sudo -u git -H yarn install --production --pure-lockfile sudo -u git -H bundle exec rake gitlab:assets:compile RAILS_ENV=production NODE_ENV=production ``` +If `rake` fails with `JavaScript heap out of memory` error, try to run it with `NODE_OPTIONS` set as follows. + +```sh +sudo -u git -H bundle exec rake gitlab:assets:compile RAILS_ENV=production NODE_ENV=production NODE_OPTIONS="--max_old_space_size=4096" +``` + ### Start Your GitLab Instance ```sh @@ -845,6 +898,25 @@ You also need to change the corresponding options (e.g. `ssh_user`, `ssh_host`, Apart from the always supported markdown style, there are other rich text files that GitLab can display. But you might have to install a dependency to do so. See the [github-markup gem README](https://github.com/gitlabhq/markup#markups) for more information. +### Using Puma + +Puma is a multi-threaded HTTP 1.1 server for Ruby applications. + +To use GitLab with Puma: + +1. Finish GitLab setup so you have it up and running. +1. Copy the supplied example Puma config file into place: + + ```sh + cd /home/git/gitlab + + # Copy config file for the web server + sudo -u git -H config/puma.rb.example config/puma.rb + ``` + +1. Edit the system `init.d` script to use `EXPERIMENTAL_PUMA=1` flag. If you have `/etc/default/gitlab`, then you should edit it instead. +1. Restart GitLab. + ## Troubleshooting ### "You appear to have cloned an empty repository." diff --git a/doc/install/ldap.md b/doc/install/ldap.md index a19f0342b65..d8d54864586 100644 --- a/doc/install/ldap.md +++ b/doc/install/ldap.md @@ -2,6 +2,4 @@ redirect_to: '../administration/auth/ldap.md' --- -# GitLab LDAP integration - -This document was moved under [`administration/auth/ldap`](../administration/auth/ldap.md). +This document was moved to [another location](../administration/auth/ldap.md). diff --git a/doc/install/openshift_and_gitlab/img/pods-overview.png b/doc/install/openshift_and_gitlab/img/pods-overview.png Binary files differdeleted file mode 100644 index 65927f65f4f..00000000000 --- a/doc/install/openshift_and_gitlab/img/pods-overview.png +++ /dev/null diff --git a/doc/install/openshift_and_gitlab/img/storage-volumes.png b/doc/install/openshift_and_gitlab/img/storage-volumes.png Binary files differdeleted file mode 100644 index 3fd092919bb..00000000000 --- a/doc/install/openshift_and_gitlab/img/storage-volumes.png +++ /dev/null diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 17099c1d051..f6a52205a0e 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -87,7 +87,7 @@ if your available memory changes. We also recommend [configuring the kernel's sw to a low value like `10` to make the most of your RAM while still having the swap available when needed. -Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as `top` or `htop`) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about how many you need of those. +NOTE: **Note:** The 25 workers of Sidekiq will show up as separate processes in your process overview (such as `top` or `htop`) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about how many you need of those. ## Database @@ -169,7 +169,7 @@ So for a machine with 2 cores, 3 unicorn workers is ideal. For all machines that have 2GB and up we recommend a minimum of three unicorn workers. If you have a 1GB machine we recommend to configure only two Unicorn workers to prevent excessive swapping. -To change the Unicorn workers when you have the Omnibus package (which defaults to the recommendation above) please see [the Unicorn settings in the Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md#unicorn-settings). +To change the Unicorn workers when you have the Omnibus package (which defaults to the recommendation above) please see [the Unicorn settings in the Omnibus GitLab documentation](https://docs.gitlab.com/omnibus/settings/unicorn.html). ## Redis and Sidekiq @@ -201,14 +201,13 @@ you decide to run GitLab Runner and the GitLab Rails application on the same machine. It is also not safe to install everything on a single machine, because of the -[security reasons] - especially when you plan to use shell executor with GitLab +[security reasons](https://docs.gitlab.com/runner/security/) +- especially when you plan to use shell executor with GitLab Runner. We recommend using a separate machine for each GitLab Runner, if you plan to use the CI features. -[security reasons]: https://gitlab.com/gitlab-org/gitlab-runner/blob/master/docs/security/index.md - ## Supported web browsers We support the current and the previous major release of: @@ -224,5 +223,5 @@ Support is only provided for the current minor version of the major version you Each time a new browser version is released, we begin supporting that version and stop supporting the third most recent version. -Note: We do not support running GitLab with JavaScript disabled in the browser and have no plans of supporting that +NOTE: **Note:** We do not support running GitLab with JavaScript disabled in the browser and have no plans of supporting that in the future because we have features such as Issue Boards which require JavaScript extensively. diff --git a/doc/integration/elasticsearch.md b/doc/integration/elasticsearch.md index 7cef664bc98..d1d12dfd064 100644 --- a/doc/integration/elasticsearch.md +++ b/doc/integration/elasticsearch.md @@ -85,7 +85,7 @@ To build and install the indexer, run: ```sh git clone https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer.git -cd /gitlab-elasticsearch-indexer +cd gitlab-elasticsearch-indexer make sudo make install ``` @@ -131,6 +131,8 @@ The following Elasticsearch settings are available: | `Use the new repository indexer (beta)` | Perform repository indexing using [GitLab Elasticsearch Indexer](https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer). | | `Search with Elasticsearch enabled` | Enables/disables using Elasticsearch in search. | | `URL` | The URL to use for connecting to Elasticsearch. Use a comma-separated list to support clustering (e.g., "http://host1, https://host2:9200"). If your Elasticsearch instance is password protected, pass the `username:password` in the URL (e.g., `http://<username>:<password>@<elastic_host>:9200/`). | +| `Number of Elasticsearch shards` | Elasticsearch indexes are split into multiple shards for performance reasons. In general, larger indexes need to have more shards. Changes to this value do not take effect until the index is recreated. You can read more about tradeoffs in the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html#create-index-settings) | +| `Number of Elasticsearch replicas` | Each Elasticsearch shard can have a number of replicas. These are a complete copy of the shard, and can provide increased query performance or resilience against hardware failure. Increasing this value will greatly increase total disk space required by the index. | | `Limit namespaces and projects that can be indexed` | Enabling this will allow you to select namespaces and projects to index. All other namespaces and projects will use database search instead. Please note that if you enable this option but do not select any namespaces or projects, none will be indexed. [Read more below](#limiting-namespaces-and-projects). | `Using AWS hosted Elasticsearch with IAM credentials` | Sign your Elasticsearch requests using [AWS IAM authorization](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html) or [AWS EC2 Instance Profile Credentials](http://docs.aws.amazon.com/codedeploy/latest/userguide/getting-started-create-iam-instance-profile.html#getting-started-create-iam-instance-profile-cli). The policies must be configured to allow `es:*` actions. | | `AWS Region` | The AWS region your Elasticsearch service is located in. | diff --git a/doc/integration/img/google_app.png b/doc/integration/img/google_app.png Binary files differdeleted file mode 100644 index 08f230452b4..00000000000 --- a/doc/integration/img/google_app.png +++ /dev/null diff --git a/doc/integration/img/salesforce_app_details.png b/doc/integration/img/salesforce_app_details.png Binary files differnew file mode 100644 index 00000000000..00e66f07282 --- /dev/null +++ b/doc/integration/img/salesforce_app_details.png diff --git a/doc/integration/img/salesforce_app_secret_details.png b/doc/integration/img/salesforce_app_secret_details.png Binary files differnew file mode 100644 index 00000000000..fad2a4a1f97 --- /dev/null +++ b/doc/integration/img/salesforce_app_secret_details.png diff --git a/doc/integration/img/salesforce_oauth_app_details.png b/doc/integration/img/salesforce_oauth_app_details.png Binary files differnew file mode 100644 index 00000000000..a5fb680cca6 --- /dev/null +++ b/doc/integration/img/salesforce_oauth_app_details.png diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index ef1f2df77f8..a13e9f73f48 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -35,6 +35,7 @@ contains some settings that are common for all providers. - [JWT](../administration/auth/jwt.md) - [OpenID Connect](../administration/auth/oidc.md) - [UltraAuth](ultra_auth.md) +- [SalesForce](salesforce.md) ## Initial OmniAuth Configuration diff --git a/doc/integration/salesforce.md b/doc/integration/salesforce.md new file mode 100644 index 00000000000..8a99641a256 --- /dev/null +++ b/doc/integration/salesforce.md @@ -0,0 +1,79 @@ +# SalesForce OmniAuth Provider + +You can integrate your GitLab instance with [SalesForce](https://www.salesforce.com/) to enable users to login to your GitLab instance with their SalesForce account. + +## Create SalesForce Application + +To enable SalesForce OmniAuth provider, you must use SalesForce's credentials for your GitLab instance. +To get the credentials (a pair of Client ID and Client Secret), you must register an application on SalesForces. + +1. Sign in to [SalesForce](https://www.salesforce.com/). + +1. Navigate to **Platform Tools/Apps/App Manager** and click on **New Connected App**. + +1. Fill in the application details into the following fields: + - **Connected App Name** and **API Name**: Set to any value but consider something like `<Organization>'s GitLab`, `<Your Name>'s GitLab`, or something else that is descriptive. + - **Description**: Description for the application. + + ![SalesForce App Details](img/salesforce_app_details.png) +1. Select **API (Enable OAuth Settings)** and click on **Enable OAuth Settings**. +1. Fill in the application details into the following fields: + - **Callback URL**: The call callback URL. For example, `https://gitlab.example.com/users/auth/salesforce/callback`. + - **Selected OAuth Scopes**: Move **Access your basic information (id, profile, email, address, phone)** and **Allow access to your unique identifier (openid)** to the right column. + + ![SalesForce Oauth App Details](img/salesforce_oauth_app_details.png) +1. Click **Save**. + +1. On your GitLab server, open the configuration file. + + For omnibus package: + + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + For installations from source: + + ```sh + cd /home/git/gitlab + sudo -u git -H editor config/gitlab.yml + ``` + +1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. + +1. Add the provider configuration: + + For omnibus package: + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + "name" => "salesforce", + "app_id" => "SALESFORCE_CLIENT_ID", + "app_secret" => "SALESFORCE_CLIENT_SECRET" + } + ] + ``` + + For installation from source: + + ``` + - { name: 'salesforce', + app_id: 'SALESFORCE_CLIENT_ID', + app_secret: 'SALESFORCE_CLIENT_SECRET' + } + ``` +1. Change `SALESFORCE_CLIENT_ID` to the Consumer Key from the SalesForce connected application page. +1. Change `SALESFORCE_CLIENT_SECRET` to the Consumer Secret from the SalesForce connected application page. + ![SalesForce App Secret Details](img/salesforce_app_secret_details.png) + +1. Save the configuration file. +1. [Reconfigure GitLab]( ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure ) or [restart GitLab]( ../administration/restart_gitlab.md#installations-from-source ) for the changes to take effect if you + installed GitLab via Omnibus or from source respectively. + +On the sign in page, there should now be a SalesForce icon below the regular sign in form. +Click the icon to begin the authentication process. SalesForce will ask the user to sign in and authorize the GitLab application. +If everything goes well, the user will be returned to GitLab and will be signed in. + +NOTE: **Note:** +GitLab requires the email address of each new user. Once the user is logged in using SalesForce, GitLab will redirect the user to the profile page where they will have to provide the email and verify the email. diff --git a/doc/license/README.md b/doc/license/README.md index 4cc387ba95f..b9281fd5299 100644 --- a/doc/license/README.md +++ b/doc/license/README.md @@ -2,4 +2,4 @@ redirect_to: 'https://docs.gitlab.com/ee/user/admin_area/license.html' --- -This document was moved to [user/admin_area/license](https://docs.gitlab.com/ee/user/admin_area/license.html). +This document was moved to [another location](https://docs.gitlab.com/ee/user/admin_area/license.html). diff --git a/doc/monitoring/performance/img/grafana_dashboard_dropdown.png b/doc/monitoring/performance/img/grafana_dashboard_dropdown.png Binary files differdeleted file mode 100644 index 51eef90068d..00000000000 --- a/doc/monitoring/performance/img/grafana_dashboard_dropdown.png +++ /dev/null diff --git a/doc/monitoring/performance/img/grafana_dashboard_import.png b/doc/monitoring/performance/img/grafana_dashboard_import.png Binary files differdeleted file mode 100644 index fd639ee0eb8..00000000000 --- a/doc/monitoring/performance/img/grafana_dashboard_import.png +++ /dev/null diff --git a/doc/monitoring/performance/img/grafana_data_source_configuration.png b/doc/monitoring/performance/img/grafana_data_source_configuration.png Binary files differdeleted file mode 100644 index a98e0ed1e7d..00000000000 --- a/doc/monitoring/performance/img/grafana_data_source_configuration.png +++ /dev/null diff --git a/doc/monitoring/performance/img/grafana_data_source_empty.png b/doc/monitoring/performance/img/grafana_data_source_empty.png Binary files differdeleted file mode 100644 index 549ada8343e..00000000000 --- a/doc/monitoring/performance/img/grafana_data_source_empty.png +++ /dev/null diff --git a/doc/monitoring/performance/img/grafana_save_icon.png b/doc/monitoring/performance/img/grafana_save_icon.png Binary files differdeleted file mode 100644 index 68a071f5ae2..00000000000 --- a/doc/monitoring/performance/img/grafana_save_icon.png +++ /dev/null diff --git a/doc/monitoring/performance/img/metrics_gitlab_configuration_settings.png b/doc/monitoring/performance/img/metrics_gitlab_configuration_settings.png Binary files differdeleted file mode 100644 index b9563a00e97..00000000000 --- a/doc/monitoring/performance/img/metrics_gitlab_configuration_settings.png +++ /dev/null diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md index 0531627b7b3..00b6c1dfdc2 100644 --- a/doc/topics/authentication/index.md +++ b/doc/topics/authentication/index.md @@ -17,11 +17,11 @@ This page gathers all the resources for the topic **Authentication** within GitL ## GitLab administrators - [LDAP (Community Edition)](../../administration/auth/ldap.md) -- [LDAP (Enterprise Edition)](https://docs.gitlab.com/ee/administration/auth/ldap-ee.html) +- [LDAP (Enterprise Edition)](https://docs.gitlab.com/ee/administration/auth/ldap-ee.html) **[STARTER]** - [Enforce Two-factor Authentication (2FA)](../../security/two_factor_authentication.md#enforce-two-factor-authentication-2fa) - **Articles:** - [How to Configure LDAP with GitLab CE](../../administration/auth/how_to_configure_ldap_gitlab_ce/index.md) - - [How to Configure LDAP with GitLab EE](https://docs.gitlab.com/ee/administration/auth/how_to_configure_ldap_gitlab_ee/index.html) + - [How to Configure LDAP with GitLab EE](https://docs.gitlab.com/ee/administration/auth/how_to_configure_ldap_gitlab_ee/index.html) **[STARTER]** - [Feature Highlight: LDAP Integration](https://about.gitlab.com/2014/07/10/feature-highlight-ldap-sync/) - [Debugging LDAP](https://about.gitlab.com/handbook/support/workflows/support-engineering/ldap/debugging_ldap.html) - **Integrations:** @@ -30,10 +30,10 @@ This page gathers all the resources for the topic **Authentication** within GitL - [Atlassian Crowd OmniAuth Provider](../../administration/auth/crowd.md) - [CAS OmniAuth Provider](../../integration/cas.md) - [SAML OmniAuth Provider](../../integration/saml.md) - - [SAML for GitLab.com Groups](https://docs.gitlab.com/ee/user/group/saml_sso/index.html) - - [SCIM user provisioning for GitLab.com Groups](https://docs.gitlab.com/ee/user/group/saml_sso/scim_setup.html) + - [SAML for GitLab.com Groups](https://docs.gitlab.com/ee/user/group/saml_sso/index.html) **[SILVER ONLY]** + - [SCIM user provisioning for GitLab.com Groups](https://docs.gitlab.com/ee/user/group/saml_sso/scim_setup.html) **[SILVER ONLY]** - [Okta SSO provider](../../administration/auth/okta.md) - - [Kerberos integration (GitLab EE)](https://docs.gitlab.com/ee/integration/kerberos.html) + - [Kerberos integration (GitLab EE)](https://docs.gitlab.com/ee/integration/kerberos.html) **[STARTER]** ## API diff --git a/doc/topics/autodevops/img/autodevops_domain_variables.png b/doc/topics/autodevops/img/autodevops_domain_variables.png Binary files differdeleted file mode 100644 index b6f8864796f..00000000000 --- a/doc/topics/autodevops/img/autodevops_domain_variables.png +++ /dev/null diff --git a/doc/topics/autodevops/img/guide_connect_cluster.png b/doc/topics/autodevops/img/guide_connect_cluster.png Binary files differdeleted file mode 100644 index 703d536f37a..00000000000 --- a/doc/topics/autodevops/img/guide_connect_cluster.png +++ /dev/null diff --git a/doc/topics/autodevops/img/guide_create_cluster.png b/doc/topics/autodevops/img/guide_create_cluster.png Binary files differdeleted file mode 100644 index cd1d0fdd8da..00000000000 --- a/doc/topics/autodevops/img/guide_create_cluster.png +++ /dev/null diff --git a/doc/topics/autodevops/img/guide_gke_apis_after.png b/doc/topics/autodevops/img/guide_gke_apis_after.png Binary files differdeleted file mode 100644 index 380de958867..00000000000 --- a/doc/topics/autodevops/img/guide_gke_apis_after.png +++ /dev/null diff --git a/doc/topics/autodevops/img/guide_gke_apis_before.png b/doc/topics/autodevops/img/guide_gke_apis_before.png Binary files differdeleted file mode 100644 index d06fc707887..00000000000 --- a/doc/topics/autodevops/img/guide_gke_apis_before.png +++ /dev/null diff --git a/doc/topics/autodevops/img/guide_merge_request_ide.png b/doc/topics/autodevops/img/guide_merge_request_ide.png Binary files differdeleted file mode 100644 index c825b0849e1..00000000000 --- a/doc/topics/autodevops/img/guide_merge_request_ide.png +++ /dev/null diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 2884458a44c..5a8744d71f9 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -771,6 +771,7 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac | `K8S_SECRET_*` | From GitLab 11.7, any variable prefixed with [`K8S_SECRET_`](#application-secret-variables) will be made available by Auto DevOps as environment variables to the deployed application. | | `KUBE_INGRESS_BASE_DOMAIN` | From GitLab 11.8, this variable can be used to set a domain per cluster. See [cluster domains](../../user/project/clusters/index.md#base-domain) for more information. | | `ROLLOUT_RESOURCE_TYPE` | From GitLab 11.9, this variable allows specification of the resource type being deployed when using a custom helm chart. Default value is `deployment`. | +| `ROLLOUT_STATUS_DISABLED` | From GitLab 12.0, this variable allows to disable rollout status check because it doesn't support all resource types, for example, `cronjob`. | | `HELM_UPGRADE_EXTRA_ARGS` | From GitLab 11.11, this variable allows extra arguments in `helm` commands when deploying the application. Note that using quotes will not prevent word splitting. | TIP: **Tip:** @@ -1099,3 +1100,7 @@ curl --data "value=true" --header "PRIVATE-TOKEN: personal_access_token" https:/ [ee]: https://about.gitlab.com/pricing/ [ce-21955]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21955 [ce-19507]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19507 + +## Development guides + +Configuring [GDK for Auto DevOps](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/auto_devops.md). diff --git a/doc/university/high-availability/aws/img/elastic-file-system.png b/doc/university/high-availability/aws/img/elastic-file-system.png Binary files differdeleted file mode 100644 index 5bcfb8d0588..00000000000 --- a/doc/university/high-availability/aws/img/elastic-file-system.png +++ /dev/null diff --git a/doc/update/mysql_to_postgresql.md b/doc/update/mysql_to_postgresql.md index e607dbceeea..b83abcd36f7 100644 --- a/doc/update/mysql_to_postgresql.md +++ b/doc/update/mysql_to_postgresql.md @@ -45,6 +45,22 @@ For other distributions, follow the instructions in PostgreSQL's [download page](https://www.postgresql.org/download/) to add their repository and then install `pgloader`. +If you are migrating to a Docker based installation, you will need to install +pgloader within the container as it is not included in the container image. + +1. Start a shell session in the context of the running container: + + ``` bash + docker exec -it gitlab bash + ``` + +1. Install pgloader: + + ``` bash + apt-get update + apt-get -y install pgloader + ``` + ## Omnibus GitLab installations For [Omnibus GitLab packages](https://about.gitlab.com/install/), you'll first diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index f90bff662e2..c2dff21b028 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -67,7 +67,7 @@ sudo -u git -H bundle exec rake gettext:pack RAILS_ENV=production sudo -u git -H bundle exec rake gettext:po_to_json RAILS_ENV=production # Clean up assets and cache -sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile cache:clear RAILS_ENV=production NODE_ENV=production +sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile cache:clear RAILS_ENV=production NODE_ENV=production NODE_OPTIONS="--max_old_space_size=4096" ``` ### 4. Update gitlab-workhorse to the corresponding version diff --git a/doc/update/upgrading_from_source.md b/doc/update/upgrading_from_source.md index fea89669831..82a86f3f343 100644 --- a/doc/update/upgrading_from_source.md +++ b/doc/update/upgrading_from_source.md @@ -25,13 +25,7 @@ This section contains all the steps necessary to upgrade Community Edition or Enterprise Edition, regardless of the version you are upgrading to. Version specific guidelines (should there be any) are covered separately. -### 1. Stop server - -```bash -sudo service gitlab stop -``` - -### 2. Backup +### 1. Backup NOTE: If you installed GitLab from source, make sure `rsync` is installed. @@ -41,6 +35,12 @@ cd /home/git/gitlab sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production ``` +### 2. Stop server + +```bash +sudo service gitlab stop +``` + ### 3. Update Ruby NOTE: Beginning in GitLab 11.6, we only support Ruby 2.5 or higher, and dropped @@ -52,9 +52,9 @@ Download Ruby and compile it: ```bash mkdir /tmp/ruby && cd /tmp/ruby -curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.5/ruby-2.5.3.tar.gz -echo 'f919a9fbcdb7abecd887157b49833663c5c15fda ruby-2.5.3.tar.gz' | shasum -c - && tar xzf ruby-2.5.3.tar.gz -cd ruby-2.5.3 +curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.6/ruby-2.6.3.tar.gz +echo '2347ed6ca5490a104ebd5684d2b9b5eefa6cd33c ruby-2.6.3.tar.gz' | shasum -c - && tar xzf ruby-2.6.3.tar.gz +cd ruby-2.6.3 ./configure --disable-install-rdoc make @@ -314,7 +314,7 @@ sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production # Update node dependencies and recompile assets -sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production +sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production NODE_OPTIONS="--max_old_space_size=4096" # Clean up cache sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production diff --git a/doc/user/admin_area/img/license_no_license_message.png b/doc/user/admin_area/img/license_no_license_message.png Binary files differdeleted file mode 100644 index 87b397f7905..00000000000 --- a/doc/user/admin_area/img/license_no_license_message.png +++ /dev/null diff --git a/doc/user/admin_area/license.md b/doc/user/admin_area/license.md index 45f986d480f..49959a9daef 100644 --- a/doc/user/admin_area/license.md +++ b/doc/user/admin_area/license.md @@ -2,7 +2,8 @@ To activate all GitLab Enterprise Edition (EE) functionality, you need to upload a license. Once you've received your license from GitLab Inc., you can upload it -by **signing into your GitLab instance as an admin**. +by **signing into your GitLab instance as an admin** or add it at +installation time. The license has the form of a base64 encoded ASCII text with a `.gitlab-license` extension and can be obtained when you [purchase one][pricing] or when you sign @@ -42,6 +43,36 @@ Otherwise, you can: "Enter license key" option, copy the license, paste it into the "License key" field and click **Upload license**. +## Add your license at install time + +The license may be automatically injected during installation using one of +two methods. + +The first requires a license file named `Gitlab.gitlab-release`. + +Place it in the `config/` directory if installing from source or in the +`/etc/gitlab/` directory if installing Omnibus. + +The second allows the administrator to configure the location and +filename of the license. + +Source installations should set the `GITLAB_LICENSE_FILE` environment +variable with the path to a valid GitLab Enterprise Edition license. + +```sh +export GITLAB_LICENSE_FILE="/path/to/license/file" +``` + +Omnibus installations should add this entry to `gitlab.rb`: + +```ruby +gitlab_rails['license_file'] = "/path/to/license/file" +``` + +CAUTION:: **Caution:** +These methods will only add a license at the time of installation. Use the +admin area in the web ui to renew or upgrade licenses. + --- Once the license is uploaded, all GitLab Enterprise Edition functionality diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md index 834a1e2423a..9dd476656ed 100644 --- a/doc/user/admin_area/settings/continuous_integration.md +++ b/doc/user/admin_area/settings/continuous_integration.md @@ -98,8 +98,7 @@ the group. NOTE: **Note:** Only available on GitLab.com. -If you have a Group with a [paid plan](https://about.gitlab.com/pricing/#gitlab-com) on GitLab.com, -then you can purchase additional CI minutes so your pipelines will not be blocked after you have +You can purchase additional CI minutes so your pipelines will not be blocked after you have used all your CI minutes from your main quota. In order to purchase additional minutes, you should follow these steps: @@ -119,7 +118,15 @@ will be synced to your Group and you can visualize it from the ![Additional minutes](img/additional_minutes.png) -NOTE: **Important note**: If you have some minutes used over your default quota, these minutes will +Be aware that: + +1. If you have purchased extra CI minutes before the purchase of a paid plan, +we will calculate a pro-rated charge for your paid plan. That means you may +be charged for less than one year since your subscription was previously +created with the extra CI minutes. +1. Once the extra CI minutes has been assigned to a Group they cannot be transferred +to a different Group. +1. If you have some minutes used over your default quota, these minutes will be deducted from your Additional Minutes quota immediately after your purchase of additional minutes. diff --git a/doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.png b/doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.png Binary files differdeleted file mode 100644 index 723be23e77b..00000000000 --- a/doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.png +++ /dev/null diff --git a/doc/user/admin_area/settings/img/admin_area_group_edit.png b/doc/user/admin_area/settings/img/admin_area_group_edit.png Binary files differdeleted file mode 100644 index c9bd2f10b36..00000000000 --- a/doc/user/admin_area/settings/img/admin_area_group_edit.png +++ /dev/null diff --git a/doc/user/admin_area/settings/img/admin_area_groups.png b/doc/user/admin_area/settings/img/admin_area_groups.png Binary files differdeleted file mode 100644 index ebdee0eafdc..00000000000 --- a/doc/user/admin_area/settings/img/admin_area_groups.png +++ /dev/null diff --git a/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png b/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png Binary files differdeleted file mode 100644 index 3f827f1f7a3..00000000000 --- a/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png +++ /dev/null diff --git a/doc/user/admin_area/settings/img/ci_shared_runners_build_minutes_quota.png b/doc/user/admin_area/settings/img/ci_shared_runners_build_minutes_quota.png Binary files differdeleted file mode 100644 index 269a3cf1fbc..00000000000 --- a/doc/user/admin_area/settings/img/ci_shared_runners_build_minutes_quota.png +++ /dev/null diff --git a/doc/user/admin_area/settings/img/group_quota_view.png b/doc/user/admin_area/settings/img/group_quota_view.png Binary files differdeleted file mode 100644 index 791bfd868e0..00000000000 --- a/doc/user/admin_area/settings/img/group_quota_view.png +++ /dev/null diff --git a/doc/user/admin_area/settings/img/group_settings.png b/doc/user/admin_area/settings/img/group_settings.png Binary files differdeleted file mode 100644 index a849d9cfdc1..00000000000 --- a/doc/user/admin_area/settings/img/group_settings.png +++ /dev/null diff --git a/doc/user/admin_area/settings/usage_statistics.md b/doc/user/admin_area/settings/usage_statistics.md index e165a120162..8b5d80efb0d 100644 --- a/doc/user/admin_area/settings/usage_statistics.md +++ b/doc/user/admin_area/settings/usage_statistics.md @@ -48,6 +48,8 @@ You can view the exact JSON payload in the administration panel. To view the pay 1. Expand **Settings** in the left sidebar and click on **Metrics and profiling**. 1. Expand **Usage statistics** and click on the **Preview payload** button. +You can see how [the usage ping data maps to different stages of the product](https://gitlab.com/gitlab-data/analytics/blob/master/transform/snowflake-dbt/data/ping_metrics_to_stage_mapping_data.csv). + ### Deactivate the usage ping The usage ping is opt-out. If you want to deactivate this feature, go to diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md index ce86ade3c1a..5c635b09503 100644 --- a/doc/user/application_security/container_scanning/index.md +++ b/doc/user/application_security/container_scanning/index.md @@ -122,12 +122,13 @@ container_scanning: ## https://docs.gitlab.com/ee/ci/variables/#predefined-environment-variables CI_APPLICATION_REPOSITORY: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG CI_APPLICATION_TAG: $CI_COMMIT_SHA + CLAIR_LOCAL_SCAN_VERSION: v2.0.8_fe9b059d930314b54c78f75afe265955faf4fdc1 allow_failure: true services: - docker:stable-dind script: - docker run -d --name db arminc/clair-db:latest - - docker run -p 6060:6060 --link db:postgres -d --name clair --restart on-failure arminc/clair-local-scan:v2.0.6 + - docker run -p 6060:6060 --link db:postgres -d --name clair --restart on-failure arminc/clair-local-scan:${CLAIR_LOCAL_SCAN_VERSION} - apk add -U wget ca-certificates - docker pull ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} - wget https://github.com/arminc/clair-scanner/releases/download/v8/clair-scanner_linux_amd64 @@ -164,12 +165,13 @@ container_scanning: ## https://docs.gitlab.com/ee/ci/variables/#predefined-environment-variables CI_APPLICATION_REPOSITORY: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG CI_APPLICATION_TAG: $CI_COMMIT_SHA + CLAIR_LOCAL_SCAN_VERSION: v2.0.8_fe9b059d930314b54c78f75afe265955faf4fdc1 allow_failure: true services: - docker:stable-dind script: - docker run -d --name db arminc/clair-db:latest - - docker run -p 6060:6060 --link db:postgres -d --name clair --restart on-failure arminc/clair-local-scan:v2.0.6 + - docker run -p 6060:6060 --link db:postgres -d --name clair --restart on-failure arminc/clair-local-scan:${CLAIR_LOCAL_SCAN_VERSION} - apk add -U wget ca-certificates - docker pull ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} - wget https://github.com/arminc/clair-scanner/releases/download/v8/clair-scanner_linux_amd64 diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md index 904c9e8fefe..f3b7d7fd471 100644 --- a/doc/user/application_security/dast/index.md +++ b/doc/user/application_security/dast/index.md @@ -138,7 +138,7 @@ variables: #### Customizing the DAST settings -The SAST settings can be changed through environment variables by using the +The DAST settings can be changed through environment variables by using the [`variables`](../../../ci/yaml/README.md#variables) parameter in `.gitlab-ci.yml`. These variables are documented in the [DAST README](https://gitlab.com/gitlab-org/security-products/dast#settings). diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md index 2d0c2be4233..5d2bb4e572b 100644 --- a/doc/user/application_security/dependency_scanning/index.md +++ b/doc/user/application_security/dependency_scanning/index.md @@ -19,8 +19,9 @@ merge request. ![Dependency Scanning Widget](img/dependency_scanning.png) -The results are sorted by the priority of the vulnerability: +The results are sorted by the severity of the vulnerability: +1. Critical 1. High 1. Medium 1. Low diff --git a/doc/user/application_security/security_dashboard/img/dashboard.png b/doc/user/application_security/security_dashboard/img/dashboard.png Binary files differindex d52a6dacdbf..a75168b1ce4 100644 --- a/doc/user/application_security/security_dashboard/img/dashboard.png +++ b/doc/user/application_security/security_dashboard/img/dashboard.png diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md index 248f8395db1..5d69efc3600 100644 --- a/doc/user/discussions/index.md +++ b/doc/user/discussions/index.md @@ -385,11 +385,6 @@ the Merge Request authored by the user that applied them. ![Add a new comment](img/insert_suggestion.png) - > **Note:** - The suggestion will only affect the commented line. Multi-line - suggestions are currently not supported. Will be introduced by - [#53310](https://gitlab.com/gitlab-org/gitlab-ce/issues/53310). - 1. In the comment, add your suggestion to the pre-populated code block: ![Add a suggestion into a code block tagged properly](img/make_suggestion.png) @@ -401,13 +396,10 @@ the Merge Request authored by the user that applied them. ![Apply suggestions](img/suggestion.png) - > **Note:** - Discussions are _not_ automatically resolved. Will be introduced by - [#54405](https://gitlab.com/gitlab-org/gitlab-ce/issues/54405). - Once the author applies a suggestion, it will be marked with the **Applied** label, -and GitLab will create a new commit with the message `Apply suggestion to <file-name>` -and push the suggested change directly into the codebase in the merge request's branch. +the discussion will be automatically resolved, and GitLab will create a new commit +with the message `Apply suggestion to <file-name>` and push the suggested change +directly into the codebase in the merge request's branch. [Developer permission](../permissions.md) is required to do so. > **Note:** diff --git a/doc/user/group/clusters/index.md b/doc/user/group/clusters/index.md index 53c82169e15..ff6aa4f5930 100644 --- a/doc/user/group/clusters/index.md +++ b/doc/user/group/clusters/index.md @@ -5,10 +5,10 @@ ## Overview -Similar to [project Kubernetes -clusters](../../project/clusters/index.md), Group-level Kubernetes -clusters allow you to connect a Kubernetes cluster to your group, -enabling you to use the same cluster across multiple projects. +Similar to [project-level](../../project/clusters/index.md) and +[instance-level](../../instance/clusters/index.md) Kubernetes clusters, +group-level Kubernetes clusters allow you to connect a Kubernetes cluster to +your group, enabling you to use the same cluster across multiple projects. ## Installing applications diff --git a/doc/user/group/custom_project_templates.md b/doc/user/group/custom_project_templates.md index 8e101407ac0..f67325272a6 100644 --- a/doc/user/group/custom_project_templates.md +++ b/doc/user/group/custom_project_templates.md @@ -1,4 +1,4 @@ -# Custom group-level project templates **[PREMIUM ONLY]** +# Custom group-level project templates **[PREMIUM]** > [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/6861) in [GitLab Premium](https://about.gitlab.com/pricing) 11.6. diff --git a/doc/user/group/dependency_proxy/img/group_dependency_proxy.png b/doc/user/group/dependency_proxy/img/group_dependency_proxy.png Binary files differnew file mode 100644 index 00000000000..035aff0b6c4 --- /dev/null +++ b/doc/user/group/dependency_proxy/img/group_dependency_proxy.png diff --git a/doc/user/group/dependency_proxy/index.md b/doc/user/group/dependency_proxy/index.md new file mode 100644 index 00000000000..4fc2d8e9509 --- /dev/null +++ b/doc/user/group/dependency_proxy/index.md @@ -0,0 +1,74 @@ +# Dependency Proxy **[PREMIUM]** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/7934) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.11. + +NOTE: **Note:** +This is the user guide. In order to use the dependency proxy, an administrator +must first [configure it](../../../administration/dependency_proxy.md). + +For many organizations, it is desirable to have a local proxy for frequently used +upstream images/packages. In the case of CI/CD, the proxy is responsible for +receiving a request and returning the upstream image from a registry, acting +as a pull-through cache. + +The dependency proxy is available in the group level. To access it, navigate to +a group's **Overview > Dependency Proxy**. + +![Dependency Proxy group page](img/group_dependency_proxy.png) + +## Supported dependency proxies + +NOTE: **Note:** +For a list of the upcoming additions to the proxies, visit the +[direction page](https://about.gitlab.com/direction/package/dependency_proxy/#top-vision-items). + +The following dependency proxies are supported. + +| Dependency proxy | GitLab version | +| ---------------- | -------------- | +| Docker | 11.11+ | + +## Using the Docker dependency proxy + +With the Docker dependency proxy, you can use GitLab as a source for a Docker image. +To get a Docker image into the dependency proxy: + +1. Find the proxy URL on your group's page under **Overview > Dependency Proxy**, + for example `gitlab.com/groupname/dependency_proxy/containers`. +1. Trigger GitLab to pull the Docker image you want (e.g., `alpine:latest` or + `linuxserver/nextcloud:latest`) and store it in the proxy storage by using + one of the following ways: + + - Manually pulling the Docker image: + + ```bash + docker pull gitlab.com/groupname/dependency_proxy/containers/alpine:latest + ``` + + - From a `Dockerfile`: + + ```bash + FROM gitlab.com/groupname/dependency_proxy/containers/alpine:latest + ``` + + - In [`.gitlab-ci.yml`](../../../ci/yaml/README.md#image): + + ```bash + image: gitlab.com/groupname/dependency_proxy/containers/alpine:latest + ``` + +GitLab will then pull the Docker image from Docker Hub and will cache the blobs +on the GitLab server. The next time you pull the same image, it will get the latest +information about the image from Docker Hub but will serve the existing blobs +from GitLab. + +The blobs are kept forever, and there is no hard limit on how much data can be +stored. + +## Limitations + +The following limitations apply: + +- Only public groups are supported (authentication is not supported yet). +- Only Docker Hub is supported. +- This feature requires Docker Hub being available. diff --git a/doc/user/group/img/group_issue_board.png b/doc/user/group/img/group_issue_board.png Binary files differdeleted file mode 100644 index a0da74a320f..00000000000 --- a/doc/user/group/img/group_issue_board.png +++ /dev/null diff --git a/doc/user/group/img/new_group_form.png b/doc/user/group/img/new_group_form.png Binary files differdeleted file mode 100644 index 1c4d3ec6ceb..00000000000 --- a/doc/user/group/img/new_group_form.png +++ /dev/null diff --git a/doc/user/group/index.md b/doc/user/group/index.md index 06564fd6cd1..a5e3bfda70e 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -368,5 +368,9 @@ and issues) performed by your group members. With [GitLab Issues Analytics](issues_analytics/index.md), in groups, you can see a bar chart of the number of issues created each month. +## Dependency Proxy **[PREMIUM]** + +Use GitLab as a [dependency proxy](dependency_proxy/index.md) for upstream Docker images. + [ee]: https://about.gitlab.com/pricing/ [ee-2534]: https://gitlab.com/gitlab-org/gitlab-ee/issues/2534 diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md index ee3137d032e..53116606201 100644 --- a/doc/user/group/saml_sso/index.md +++ b/doc/user/group/saml_sso/index.md @@ -22,8 +22,16 @@ SAML SSO for groups is used only as a convenient way to add users and does not s ![Issuer and callback for configuring SAML identity provider with GitLab.com](img/group_saml_configuration_information.png) -NOTE: **Note:** -Partial SSO enforcement was introduced in [11.8](https://gitlab.com/gitlab-org/gitlab-ee/issues/5291). With this option enabled, users must use your group's GitLab single sign on URL to be added to the group or be added via SCIM. Users can no longer be added manually. After a user has been added to the group, GitLab does not continue to enforce the use of SSO, but we'll [add a persistent check](https://gitlab.com/gitlab-org/gitlab-ee/issues/9255) in a later version. +### SSO enforcement + +SSO enforcement was: + +- [Introduced in GitLab 11.8](https://gitlab.com/gitlab-org/gitlab-ee/issues/5291). +- [Improved upon in GitLab 11.11 with ongoing enforcement in the GitLab UI](https://gitlab.com/gitlab-org/gitlab-ee/issues/9255). + +With this option enabled, users must use your group's GitLab single sign on URL to be added to the group or be added via SCIM. Users cannot be added manually, and may only access project/group resources via the UI by signing in through the SSO URL. + +We intend to add a similar SSO requirement for [Git and API activity](https://gitlab.com/gitlab-org/gitlab-ee/issues/9152) in the future. ### NameID diff --git a/doc/user/img/mermaid_diagram_render_gfm.png b/doc/user/img/mermaid_diagram_render_gfm.png Binary files differdeleted file mode 100644 index 9d192a30a85..00000000000 --- a/doc/user/img/mermaid_diagram_render_gfm.png +++ /dev/null diff --git a/doc/user/instance/clusters/index.md b/doc/user/instance/clusters/index.md new file mode 100644 index 00000000000..894f83d3c75 --- /dev/null +++ b/doc/user/instance/clusters/index.md @@ -0,0 +1,23 @@ +# Instance-level Kubernetes clusters + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/39840) in GitLab 11.11. +> Instance-level cluster integration is currently in [Beta](https://about.gitlab.com/handbook/product/#alpha-beta-ga). + +## Overview + +Similar to [project-level](../../project/clusters/index.md) +and [group-level](../../group/clusters/index.md) Kubernetes clusters, +instance-level Kubernetes clusters allow you to connect a Kubernetes cluster to +the GitLab instance, which enables you to use the same cluster across multiple +projects. + +## Cluster precedence + +GitLab will try match to clusters in the following order: + +- Project-level clusters +- Group-level clusters +- Instance level + +To be selected, the cluster must be enabled and +match the [environment selector](../../../ci/environments.md#scoping-environments-with-specs-premium). diff --git a/doc/user/markdown.md b/doc/user/markdown.md index c1c2c036bae..5dad9621802 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -286,15 +286,15 @@ On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/he Ubuntu 18.04 (like many modern Linux distros) has this font installed by default. ``` -Sometimes you want to <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/public/-/emojis/1/monkey.png" width="20px" height="20px" style="display:inline;margin:0"> around a bit and add some <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/public/-/emojis/1/star2.png" width="20px" height="20px" style="display:inline;margin:0"> to your <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/public/-/emojis/1/speech_balloon.png" width="20px" height="20px" style="display:inline;margin:0">. Well we have a gift for you: +Sometimes you want to <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/monkey.png" width="20px" height="20px" style="display:inline;margin:0"> around a bit and add some <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/star2.png" width="20px" height="20px" style="display:inline;margin:0"> to your <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/speech_balloon.png" width="20px" height="20px" style="display:inline;margin:0">. Well we have a gift for you: -<img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/public/-/emojis/1/zap.png" width="20px" height="20px" style="display:inline;margin:0">You can use emoji anywhere GFM is supported. <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/public/-/emojis/1/v.png" width="20px" height="20px" style="display:inline;margin:0"> +<img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/zap.png" width="20px" height="20px" style="display:inline;margin:0">You can use emoji anywhere GFM is supported. <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/v.png" width="20px" height="20px" style="display:inline;margin:0"> -You can use it to point out a <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/public/-/emojis/1/bug.png" width="20px" height="20px" style="display:inline;margin:0"> or warn about <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/public/-/emojis/1/speak_no_evil.png" width="20px" height="20px" style="display:inline;margin:0"> patches. And if someone improves your really <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/public/-/emojis/1/snail.png" width="20px" height="20px" style="display:inline;margin:0"> code, send them some <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/public/-/emojis/1/birthday.png" width="20px" height="20px" style="display:inline;margin:0">. People will <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/public/-/emojis/1/heart.png" width="20px" height="20px" style="display:inline;margin:0"> you for that. +You can use it to point out a <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/bug.png" width="20px" height="20px" style="display:inline;margin:0"> or warn about <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/speak_no_evil.png" width="20px" height="20px" style="display:inline;margin:0"> patches. And if someone improves your really <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/snail.png" width="20px" height="20px" style="display:inline;margin:0"> code, send them some <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/birthday.png" width="20px" height="20px" style="display:inline;margin:0">. People will <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/heart.png" width="20px" height="20px" style="display:inline;margin:0"> you for that. -If you are new to this, don't be <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/public/-/emojis/1/fearful.png" width="20px" height="20px" style="display:inline;margin:0">. You can easily join the emoji <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/public/-/emojis/1/family.png" width="20px" height="20px" style="display:inline;margin:0">. All you need to do is to look up one of the supported codes. +If you are new to this, don't be <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/fearful.png" width="20px" height="20px" style="display:inline;margin:0">. You can easily join the emoji <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/family.png" width="20px" height="20px" style="display:inline;margin:0">. All you need to do is to look up one of the supported codes. -Consult the [Emoji Cheat Sheet](https://www.webfx.com/tools/emoji-cheat-sheet/) for a list of all supported emoji codes. <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/public/-/emojis/1/thumbsup.png" width="20px" height="20px" style="display:inline;margin:0"> +Consult the [Emoji Cheat Sheet](https://www.webfx.com/tools/emoji-cheat-sheet/) for a list of all supported emoji codes. <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/thumbsup.png" width="20px" height="20px" style="display:inline;margin:0"> Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support. @@ -480,7 +480,7 @@ GitLab 10.3. > If this is not rendered correctly, see https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#mermaid -It is possible to generate diagrams and flowcharts from text using [Mermaid][mermaid]. +It is possible to generate diagrams and flowcharts from text using [Mermaid](https://mermaidjs.github.io/). In order to generate a diagram or flowchart, you should write your text inside the `mermaid` block. @@ -496,9 +496,15 @@ Example: Becomes: -<img src="./img/mermaid_diagram_render_gfm.png" width="200px" height="400px"> +```mermaid +graph TD; + A-->B; + A-->C; + B-->D; + C-->D; +``` -For details see the [Mermaid official page][mermaid]. +For details see the [Mermaid official page](https://mermaidjs.github.io/). ### Front matter @@ -1072,7 +1078,6 @@ A link starting with a `/` is relative to the wiki root. [^2]: This is my awesome footnote. [markdown.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md -[mermaid]: https://mermaidjs.github.io/ "Mermaid website" [rouge]: http://rouge.jneen.net/ "Rouge website" [redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website" [katex]: https://github.com/Khan/KaTeX "KaTeX website" diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 9b298e4eb30..318053fdabb 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -11,7 +11,7 @@ project itself, the highest permission level is used. On public and internal projects the Guest role is not enforced. All users will be able to create issues, leave comments, and clone or download the project code. -When a member leaves the team all the assigned [Issues](project/issues/index.md) and [Merge Requests](project/merge_requests/index.md) +When a member leaves a team's project, all the assigned [Issues](project/issues/index.md) and [Merge Requests](project/merge_requests/index.md) will be unassigned automatically. GitLab [administrators](../administration/index.md) receive all permissions. diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index 157afb3a78c..3bc3beb2055 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -19,8 +19,10 @@ or provide the credentials to an [existing Kubernetes cluster](#adding-an-existi NOTE: **Note:** From [GitLab 11.6](https://gitlab.com/gitlab-org/gitlab-ce/issues/34758) you -can also associate a Kubernetes cluster to your groups. Learn more about -[group Kubernetes clusters](../../group/clusters/index.md). +can also associate a Kubernetes cluster to your groups and from +[GitLab 11.11](https://gitlab.com/gitlab-org/gitlab-ce/issues/39840), +to the GitLab instance. Learn more about [group-level](../../group/clusters/index.md) +and [instance-level](../../instance/clusters/index.md) Kubernetes clusters. ## Adding and creating a new GKE cluster via GitLab @@ -423,9 +425,9 @@ To uninstall an application: 1. Select your cluster. 1. Click the **Uninstall** button for the application. -Support for uninstalling all applications will be progressively -introduced (see [related -epic](https://gitlab.com/groups/gitlab-org/-/epics/1201)). +Support for uninstalling all applications is planned for progressive rollout. +To follow progress, see [the relevant +epic](https://gitlab.com/groups/gitlab-org/-/epics/1201). ### Troubleshooting applications @@ -674,11 +676,6 @@ To remove the Kubernetes cluster integration from your project, simply click the **Remove integration** button. You will then be able to follow the procedure and add a Kubernetes cluster again. -## View Kubernetes pod logs from GitLab **[ULTIMATE]** - -Learn how to easily -[view the logs of running pods in connected Kubernetes clusters](kubernetes_pod_logs.md). - ## What you can get with the Kubernetes integration Here's what you can do with GitLab if you enable the Kubernetes integration. @@ -701,6 +698,12 @@ the need to leave GitLab. [Read more about Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html) +### Pod logs **[ULTIMATE]** + +GitLab makes it easy to view the logs of running pods in connected Kubernetes clusters. By displaying the logs directly in GitLab, developers can avoid having to manage console tools or jump to a different interface. + +[Read more about Kubernetes pod logs](kubernetes_pod_logs.md) + ### Kubernetes monitoring Automatically detect and monitor Kubernetes metrics. Automatic monitoring of diff --git a/doc/user/project/code_owners.md b/doc/user/project/code_owners.md index a4937e6cf6b..ae04616943f 100644 --- a/doc/user/project/code_owners.md +++ b/doc/user/project/code_owners.md @@ -43,7 +43,7 @@ Example `CODEOWNERS` file: # app/ @commented-rule -# We can specifiy a default match using wildcards: +# We can specify a default match using wildcards: * @default-codeowner # Rules defined later in the file take precedence over the rules diff --git a/doc/user/project/img/assignee_lists.png b/doc/user/project/img/assignee_lists.png Binary files differdeleted file mode 100644 index f2660cd8f80..00000000000 --- a/doc/user/project/img/assignee_lists.png +++ /dev/null diff --git a/doc/user/project/img/issue_board.png b/doc/user/project/img/issue_board.png Binary files differdeleted file mode 100644 index b753593d212..00000000000 --- a/doc/user/project/img/issue_board.png +++ /dev/null diff --git a/doc/user/project/img/labels_sidebar_inline.png b/doc/user/project/img/labels_sidebar_inline.png Binary files differdeleted file mode 100644 index 2186f14ea92..00000000000 --- a/doc/user/project/img/labels_sidebar_inline.png +++ /dev/null diff --git a/doc/user/project/img/priority_sort_order.png b/doc/user/project/img/priority_sort_order.png Binary files differdeleted file mode 100644 index cd1dd8237c0..00000000000 --- a/doc/user/project/img/priority_sort_order.png +++ /dev/null diff --git a/doc/user/project/img/project_security_dashboard.png b/doc/user/project/img/project_security_dashboard.png Binary files differdeleted file mode 100644 index 3294e59e943..00000000000 --- a/doc/user/project/img/project_security_dashboard.png +++ /dev/null diff --git a/doc/user/project/img/protected_branches_choose_branch.png b/doc/user/project/img/protected_branches_choose_branch.png Binary files differdeleted file mode 100644 index c2848db9c96..00000000000 --- a/doc/user/project/img/protected_branches_choose_branch.png +++ /dev/null diff --git a/doc/user/project/img/protected_branches_error_ui.png b/doc/user/project/img/protected_branches_error_ui.png Binary files differdeleted file mode 100644 index 62839e49d89..00000000000 --- a/doc/user/project/img/protected_branches_error_ui.png +++ /dev/null diff --git a/doc/user/project/import/img/import_projects_from_github_select_auth_method.png b/doc/user/project/import/img/import_projects_from_github_select_auth_method.png Binary files differdeleted file mode 100644 index 90e6243aec0..00000000000 --- a/doc/user/project/import/img/import_projects_from_github_select_auth_method.png +++ /dev/null diff --git a/doc/user/project/integrations/img/jira_project_name.png b/doc/user/project/integrations/img/jira_project_name.png Binary files differdeleted file mode 100644 index 981c7f7ca18..00000000000 --- a/doc/user/project/integrations/img/jira_project_name.png +++ /dev/null diff --git a/doc/user/project/integrations/img/jira_service.png b/doc/user/project/integrations/img/jira_service.png Binary files differdeleted file mode 100644 index 0ae2fa28756..00000000000 --- a/doc/user/project/integrations/img/jira_service.png +++ /dev/null diff --git a/doc/user/project/integrations/img/prometheus_yaml_deploy.png b/doc/user/project/integrations/img/prometheus_yaml_deploy.png Binary files differdeleted file mode 100644 index 78dd178a077..00000000000 --- a/doc/user/project/integrations/img/prometheus_yaml_deploy.png +++ /dev/null diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index 8e1603f9ec9..10eb3a4f3b7 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -321,8 +321,14 @@ X-Gitlab-Event: Issue Hook "group_id": 41 }], "changes": { - "updated_by_id": [null, 1], - "updated_at": ["2017-09-15 16:50:55 UTC", "2017-09-15 16:52:00 UTC"], + "updated_by_id": { + "previous": null, + "current": 1 + }, + "updated_at": { + "previous": "2017-09-15 16:50:55 UTC", + "current": "2017-09-15 16:52:00 UTC" + }, "labels": { "previous": [{ "id": 206, @@ -851,8 +857,14 @@ X-Gitlab-Event: Merge Request Hook "group_id": 41 }], "changes": { - "updated_by_id": [null, 1], - "updated_at": ["2017-09-15 16:50:55 UTC", "2017-09-15 16:52:00 UTC"], + "updated_by_id": { + "previous": null, + "current": 1 + }, + "updated_at": { + "previous": "2017-09-15 16:50:55 UTC", + "current":"2017-09-15 16:52:00 UTC" + }, "labels": { "previous": [{ "id": 206, diff --git a/doc/user/project/issues/csv_import.md b/doc/user/project/issues/csv_import.md index 032e3a73ad0..b0b1cfcfdf7 100644 --- a/doc/user/project/issues/csv_import.md +++ b/doc/user/project/issues/csv_import.md @@ -1,17 +1,23 @@ -# Importing Issues from CSV +# Importing issues from CSV > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/23532) in GitLab 11.7. -Issues can be imported to a project by uploading a CSV file. Supported fields are -`title` and `description`. +Issues can be imported to a project by uploading a CSV file with the columns +`title` and `description`, in that order. The user uploading the CSV file will be set as the author of the imported issues. > **Note:** A permission level of `Developer` or higher is required to import issues. +## Prepare for the import + +- Consider importing a test file containing only a few issues. There is no way to undo a large import without using the GitLab API. +- Ensure your CSV file meets the [file format](#csv-file-format) requirements. + +## Import the file + To import issues: -1. Ensure your CSV file meets the [file format](#csv-file-format) requirements. 1. Navigate to a project's Issues list page. 1. If existing issues are present, click the import icon at the top right, next to the **Edit issues** button. 1. For a project without any issues, click the button labeled **Import CSV** in the middle of the page. @@ -20,11 +26,11 @@ To import issues: The file is processed in the background and a notification email is sent to you once the import is completed. -## CSV File Format +## CSV file format ### Header row -CSV files must contain a header row beginning with at least two columns, `title` and `description`, in that order. +CSV files must contain a header row where the first column header is `title` and the second is `description`. If additional columns are present, they will be ignored. ### Column separator @@ -53,7 +59,7 @@ The limit depends on the configuration value of Max Attachment Size for the GitL For GitLab.com, it is set to 10 MB. -## Sample Data +## Sample data ```csv title,description diff --git a/doc/user/project/issues/img/group_issues_list_view.png b/doc/user/project/issues/img/group_issues_list_view.png Binary files differdeleted file mode 100644 index c951a9e2dcd..00000000000 --- a/doc/user/project/issues/img/group_issues_list_view.png +++ /dev/null diff --git a/doc/user/project/issues/img/issue_template.png b/doc/user/project/issues/img/issue_template.png Binary files differdeleted file mode 100644 index 6cb2c07d27e..00000000000 --- a/doc/user/project/issues/img/issue_template.png +++ /dev/null diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md index ac91cd4ea98..8e9e9aa79cf 100644 --- a/doc/user/project/labels.md +++ b/doc/user/project/labels.md @@ -131,7 +131,7 @@ From the project issue list page and the project merge request list page, you ca From the group issue list page and the group merge request list page, you can [filter](../search/index.md#issues-and-merge-requests) by both group labels (including subgroup ancestors and subgroup descendants) and project labels. -From the group epic list page, you can [filter](../search/index.md#issues-and-merge-requests) by both current group labels as well as decendent group labels. +From the group epic list page, you can [filter](../search/index.md#issues-and-merge-requests) by both current group labels as well as descendant group labels. ![Labels group issues](img/labels_group_issues.png) diff --git a/doc/user/project/members/img/add_new_user_to_project_settings.png b/doc/user/project/members/img/add_new_user_to_project_settings.png Binary files differdeleted file mode 100644 index e49ea1a3e3d..00000000000 --- a/doc/user/project/members/img/add_new_user_to_project_settings.png +++ /dev/null diff --git a/doc/user/project/members/img/add_user_members_menu.png b/doc/user/project/members/img/add_user_members_menu.png Binary files differdeleted file mode 100644 index 6f08088b52f..00000000000 --- a/doc/user/project/members/img/add_user_members_menu.png +++ /dev/null diff --git a/doc/user/project/members/img/max_access_level.png b/doc/user/project/members/img/max_access_level.png Binary files differdeleted file mode 100644 index 42a0416ffbb..00000000000 --- a/doc/user/project/members/img/max_access_level.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/container_scanning.png b/doc/user/project/merge_requests/img/container_scanning.png Binary files differdeleted file mode 100644 index e47f62acd9d..00000000000 --- a/doc/user/project/merge_requests/img/container_scanning.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/create-issue-with-list-hover.png b/doc/user/project/merge_requests/img/create-issue-with-list-hover.png Binary files differdeleted file mode 100644 index 7d70e8299f5..00000000000 --- a/doc/user/project/merge_requests/img/create-issue-with-list-hover.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/dast_all.png b/doc/user/project/merge_requests/img/dast_all.png Binary files differdeleted file mode 100644 index b6edc928dc3..00000000000 --- a/doc/user/project/merge_requests/img/dast_all.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/dast_single.png b/doc/user/project/merge_requests/img/dast_single.png Binary files differdeleted file mode 100644 index 26ca4bde786..00000000000 --- a/doc/user/project/merge_requests/img/dast_single.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/dependency_scanning.png b/doc/user/project/merge_requests/img/dependency_scanning.png Binary files differdeleted file mode 100644 index 18df356f846..00000000000 --- a/doc/user/project/merge_requests/img/dependency_scanning.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/interactive_reports.png b/doc/user/project/merge_requests/img/interactive_reports.png Binary files differdeleted file mode 100644 index 9f9812dc69d..00000000000 --- a/doc/user/project/merge_requests/img/interactive_reports.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/license_management.png b/doc/user/project/merge_requests/img/license_management.png Binary files differdeleted file mode 100644 index cdce6b5fe38..00000000000 --- a/doc/user/project/merge_requests/img/license_management.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/license_management_decision.png b/doc/user/project/merge_requests/img/license_management_decision.png Binary files differdeleted file mode 100644 index 0763130c375..00000000000 --- a/doc/user/project/merge_requests/img/license_management_decision.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/license_management_pipeline_tab.png b/doc/user/project/merge_requests/img/license_management_pipeline_tab.png Binary files differdeleted file mode 100644 index 80ffca815b9..00000000000 --- a/doc/user/project/merge_requests/img/license_management_pipeline_tab.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/license_management_settings.png b/doc/user/project/merge_requests/img/license_management_settings.png Binary files differdeleted file mode 100644 index b5490e59074..00000000000 --- a/doc/user/project/merge_requests/img/license_management_settings.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/revert_changes_commit.png b/doc/user/project/merge_requests/img/revert_changes_commit.png Binary files differdeleted file mode 100644 index c9dd0019024..00000000000 --- a/doc/user/project/merge_requests/img/revert_changes_commit.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/revert_changes_mr.png b/doc/user/project/merge_requests/img/revert_changes_mr.png Binary files differdeleted file mode 100644 index 06b841b3002..00000000000 --- a/doc/user/project/merge_requests/img/revert_changes_mr.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/sast.png b/doc/user/project/merge_requests/img/sast.png Binary files differdeleted file mode 100644 index 2c75592c32a..00000000000 --- a/doc/user/project/merge_requests/img/sast.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/security_report.png b/doc/user/project/merge_requests/img/security_report.png Binary files differdeleted file mode 100644 index ba41b707238..00000000000 --- a/doc/user/project/merge_requests/img/security_report.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/vulnerability_solution.png b/doc/user/project/merge_requests/img/vulnerability_solution.png Binary files differdeleted file mode 100644 index 7443b9b6eea..00000000000 --- a/doc/user/project/merge_requests/img/vulnerability_solution.png +++ /dev/null diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index 2bb2d906453..723195224d0 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -397,6 +397,14 @@ GitLab can scan and report any vulnerabilities found in your project. [Read more about security reports.](https://docs.gitlab.com/ee/user/application_security/index.html) +## JUnit test reports + +Configure your CI jobs to use JUnit test reports, and let GitLab display a report +on the merge request so that it’s easier and faster to identify the failure +without having to check the entire job log. + +[Read more about JUnit test reports](../../../ci/junit_test_reports.md). + ## Live preview with Review Apps If you configured [Review Apps](https://about.gitlab.com/features/review-apps/) for your project, diff --git a/doc/user/project/merge_requests/merge_request_approvals.md b/doc/user/project/merge_requests/merge_request_approvals.md index 265871a7b4b..d0291c4cef5 100644 --- a/doc/user/project/merge_requests/merge_request_approvals.md +++ b/doc/user/project/merge_requests/merge_request_approvals.md @@ -77,7 +77,7 @@ request approval rules: 1. Navigate to your project's **Settings > General** and expand **Merge request approvals**. 1. Click **Add approvers** to create a new approval rule. -1. Just like in [GitLab Starter](#editing-approvals), select the approval members and aprovals required. +1. Just like in [GitLab Starter](#editing-approvals), select the approval members and approvals required. 1. Give the approval rule a name that describes the set of approvers selected. 1. Click **Add approvers** to submit the new rule. @@ -173,8 +173,7 @@ the merge request. To enable this feature: 1. Navigate to your project's **Settings > General** and expand **Merge request approvals**. -1. Tick the **Require approval from code owners** checkbox - checkbox. +1. Tick the **Require approval from code owners** checkbox. 1. Click **Save changes**. When this feature is enabled, all merge requests will need approval @@ -294,6 +293,18 @@ enabling [**Prevent approval of merge requests by their committers**](#prevent-a 1. Tick the checkbox **Prevent approval of merge requests by their committers**. 1. Click **Save changes**. +## Require authentication when approving a merge request **[STARTER]** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5981) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.0. + +You can force the approver to enter a password in order to authenticate who is approving the merge request by +enabling **Require user password to approve**. This enables an Electronic Signature +for approvals such as the one defined by [CFR Part 11](https://www.accessdata.fda.gov/scripts/cdrh/cfdocs/cfcfr/CFRSearch.cfm?CFRPart=11&showFR=1&subpartNode=21:1.0.1.1.8.3)): + +1. Navigate to your project's **Settings > General** and expand **Merge request approvals**. +1. Tick the checkbox **Require user password to approve**. +1. Click **Save changes**. + ## Merge requests with different source branch and target branch projects If the merge request source branch and target branch belong to different diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md index 6c3fa5eb463..d36312c9b8d 100644 --- a/doc/user/project/new_ci_build_permissions_model.md +++ b/doc/user/project/new_ci_build_permissions_model.md @@ -44,14 +44,14 @@ It is important to note that we have a few types of users: - **Administrators**: CI jobs created by Administrators will not have access to all GitLab projects, but only to projects and container images of projects - that the administrator is a member of.That means that if a project is either + that the administrator is a member of. That means that if a project is either public or internal users have access anyway, but if a project is private, the Administrator will have to be a member of it in order to have access to it via another project's job. - **External users**: CI jobs created by [external users](../permissions.md#external-users-permissions) will have access only to projects to which user has at least reporter access. This - rules out accessing all internal projects by default, + rules out accessing all internal projects by default. This allows us to make the CI and permission system more trustworthy. Let's consider the following scenario: diff --git a/doc/user/project/pages/img/pages_create_project.png b/doc/user/project/pages/img/pages_create_project.png Binary files differdeleted file mode 100644 index 69e84b84984..00000000000 --- a/doc/user/project/pages/img/pages_create_project.png +++ /dev/null diff --git a/doc/user/project/pages/img/pages_create_user_page.png b/doc/user/project/pages/img/pages_create_user_page.png Binary files differdeleted file mode 100644 index 2f1a19ae424..00000000000 --- a/doc/user/project/pages/img/pages_create_user_page.png +++ /dev/null diff --git a/doc/user/project/pages/img/pages_dns_details.png b/doc/user/project/pages/img/pages_dns_details.png Binary files differdeleted file mode 100644 index 3e57f43f7ba..00000000000 --- a/doc/user/project/pages/img/pages_dns_details.png +++ /dev/null diff --git a/doc/user/project/pages/img/pages_multiple_domains.png b/doc/user/project/pages/img/pages_multiple_domains.png Binary files differdeleted file mode 100644 index 76c39101439..00000000000 --- a/doc/user/project/pages/img/pages_multiple_domains.png +++ /dev/null diff --git a/doc/user/project/pages/img/pages_new_domain_button.png b/doc/user/project/pages/img/pages_new_domain_button.png Binary files differdeleted file mode 100644 index cd59defa006..00000000000 --- a/doc/user/project/pages/img/pages_new_domain_button.png +++ /dev/null diff --git a/doc/user/project/pages/img/pages_upload_cert.png b/doc/user/project/pages/img/pages_upload_cert.png Binary files differdeleted file mode 100644 index 64e5f8eced1..00000000000 --- a/doc/user/project/pages/img/pages_upload_cert.png +++ /dev/null diff --git a/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md b/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md index ea22f3e905b..da1b7c59c8e 100644 --- a/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md +++ b/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md @@ -145,8 +145,8 @@ Now that your certificate has been issued, let's add it to your Pages site: 1. Visit your website at `https://example.com`. To force `https` connections on your site, navigate to your -project's **Settings > Pages** and check **Force domains with SSL -certificates to use HTTPS**. +project's **Settings > Pages** and check **Force HTTPS (requires +valid certificates)**. ## Renewal diff --git a/doc/user/project/web_ide/img/enable_web_ide.png b/doc/user/project/web_ide/img/enable_web_ide.png Binary files differdeleted file mode 100644 index 196baa82ad2..00000000000 --- a/doc/user/project/web_ide/img/enable_web_ide.png +++ /dev/null diff --git a/doc/user/project/wiki/index.md b/doc/user/project/wiki/index.md index 95f606fd786..e3c6cd6d6ff 100644 --- a/doc/user/project/wiki/index.md +++ b/doc/user/project/wiki/index.md @@ -116,7 +116,9 @@ instructions. ## Customizing sidebar -By default, the wiki would render a sidebar which lists all the pages for the -wiki. You could as well provide a `_sidebar` page to replace this default -sidebar. When this customized sidebar page is provided, the default sidebar -would not be rendered, but the customized one. +On the project's Wiki page, there is a right side navigation that renders the full Wiki pages list by default, with hierarchy. + +If the Wiki repository contains a `_sidebar` page, the file of this page replaces the default side navigation. +This custom file serves to render it's custom content, fully replacing the standard sidebar. + +Support for displaying a generated TOC with a custom side navigation is planned. diff --git a/doc/user/search/img/issues_any_assignee.png b/doc/user/search/img/issues_any_assignee.png Binary files differdeleted file mode 100644 index 2f902bcc66c..00000000000 --- a/doc/user/search/img/issues_any_assignee.png +++ /dev/null diff --git a/doc/user/search/img/issues_author.png b/doc/user/search/img/issues_author.png Binary files differdeleted file mode 100644 index 792f9746db6..00000000000 --- a/doc/user/search/img/issues_author.png +++ /dev/null diff --git a/doc/workflow/award_emoji.png b/doc/workflow/award_emoji.png Binary files differdeleted file mode 100644 index 1ad634a343e..00000000000 --- a/doc/workflow/award_emoji.png +++ /dev/null diff --git a/doc/workflow/img/copy_ssh_public_key_button.png b/doc/workflow/img/copy_ssh_public_key_button.png Binary files differnew file mode 100644 index 00000000000..e20dae09a4d --- /dev/null +++ b/doc/workflow/img/copy_ssh_public_key_button.png diff --git a/doc/workflow/img/new_branch_from_issue.png b/doc/workflow/img/new_branch_from_issue.png Binary files differdeleted file mode 100644 index 286d775bb9e..00000000000 --- a/doc/workflow/img/new_branch_from_issue.png +++ /dev/null diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md index da0243705aa..202f2e39975 100644 --- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md +++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md @@ -250,6 +250,7 @@ If you are storing LFS files outside of GitLab you can disable LFS on the projec It is possible to host LFS objects externally by setting a custom LFS url with `git config -f .lfsconfig lfs.url https://example.com/<project>.git/info/lfs`. -Because GitLab verifies the existence of objects referenced by LFS pointers, push will fail when LFS is enabled for the project. +You might choose to do this if you are using an appliance like a Sonatype Nexus to store LFS data. If you choose to use an external LFS store, +GitLab will not be able to verify LFS objects which means that pushes will fail if you have GitLab LFS support enabled. -LFS can be disabled from the [Project settings](../../user/project/settings/index.md). +To stop push failure, LFS support can be disabled in the [Project settings](../../user/project/settings/index.md). This means you will lose GitLab LFS value-adds (Verifying LFS objects, UI integration for LFS). diff --git a/doc/workflow/repository_mirroring.md b/doc/workflow/repository_mirroring.md index 9fcadbf3bee..2f8f1545b84 100644 --- a/doc/workflow/repository_mirroring.md +++ b/doc/workflow/repository_mirroring.md @@ -222,8 +222,10 @@ being injected into your mirror, or your password being stolen. ### SSH public key authentication To use SSH public key authentication, you'll also need to choose that option -from the **Authentication method** dropdown. GitLab will generate a 4096-bit RSA -key and display the public component of that key to you. +from the **Authentication method** dropdown. When the mirror is created, +GitLab generates a 4096-bit RSA key that can be copied by clicking the **Copy SSH public key** button. + +![Repository mirroring copy SSH public key to clipboard button](img/copy_ssh_public_key_button.png) You then need to add the public SSH key to the other repository's configuration: diff --git a/doc/workflow/share_with_group.png b/doc/workflow/share_with_group.png Binary files differdeleted file mode 100644 index 2c47625e29a..00000000000 --- a/doc/workflow/share_with_group.png +++ /dev/null diff --git a/doc/workflow/timezone.md b/doc/workflow/timezone.md index 338b3a32265..da51c0f2c93 100644 --- a/doc/workflow/timezone.md +++ b/doc/workflow/timezone.md @@ -1,31 +1,39 @@ # Changing your time zone The global time zone configuration parameter can be changed in `config/gitlab.yml`: + ``` - # time_zone: 'UTC' +# time_zone: 'UTC' ``` -Uncomment and customize if you want to change the default time zone of GitLab application. +Uncomment and customize if you want to change the default time zone of the GitLab application. + + +## Viewing available timezones To see all available time zones, run `bundle exec rake time:zones:all`. -With Omnibus installations, run `gitlab-rake time:zones:all`. +For Omnibus installations, run `gitlab-rake time:zones:all`. + +NOTE: **Note:** +Currently, this rake task does not list timezones in TZInfo format required by GitLab Omnibus during a reconfigure: [#58672](https://gitlab.com/gitlab-org/gitlab-ce/issues/58672). ## Changing time zone in omnibus installations GitLab defaults its time zone to UTC. It has a global timezone configuration parameter in `/etc/gitlab/gitlab.rb`. -To update, add the time zone that best applies to your location. Here are two examples: +To obtain a list of timezones, log in to your GitLab application server and run a command that generates a list of timezones in TZInfo format for the server. For example, install `timedatectl` and run `timedatectl list-timezones`. + +To update, add the timezone that best applies to your location. For example: + ``` gitlab_rails['time_zone'] = 'America/New_York' ``` -or -``` -gitlab_rails['time_zone'] = 'Europe/Brussels' -``` -After you added this field, reconfigure and restart: +After adding the configuration parameter, reconfigure and restart your GitLab instance: + ``` gitlab-ctl reconfigure gitlab-ctl restart ``` + diff --git a/fixtures/emojis/index.json b/fixtures/emojis/index.json index f55571d31fa..0406d404532 100644 --- a/fixtures/emojis/index.json +++ b/fixtures/emojis/index.json @@ -16344,7 +16344,7 @@ "aliases": [], "aliases_ascii": [], "keywords": [ - "accomodation", + "accommodation", "building", "checkin", "whotel", diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb index 8afe6dda414..5928ee1657b 100644 --- a/lib/api/discussions.rb +++ b/lib/api/discussions.rb @@ -134,9 +134,13 @@ module API post ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes" do noteable = find_noteable(parent_type, noteables_str, params[:noteable_id]) notes = readable_discussion_notes(noteable, params[:discussion_id]) + first_note = notes.first break not_found!("Discussion") if notes.empty? - break bad_request!("Discussion is an individual note.") unless notes.first.part_of_discussion? + + unless first_note.part_of_discussion? || first_note.to_discussion.can_convert_to_discussion? + break bad_request!("Discussion can not be replied to.") + end opts = { note: params[:body], diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 90ed24a2ded..625fada4f08 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -542,10 +542,15 @@ module API class IssueBasic < ProjectEntity expose :closed_at expose :closed_by, using: Entities::UserBasic - expose :labels do |issue| - # Avoids an N+1 query since labels are preloaded - issue.labels.map(&:title).sort + + expose :labels do |issue, options| + if options[:with_labels_details] + ::API::Entities::LabelBasic.represent(issue.labels.sort_by(&:title)) + else + issue.labels.map(&:title).sort + end end + expose :milestone, using: Entities::Milestone expose :assignees, :author, using: Entities::UserBasic @@ -573,6 +578,14 @@ module API class Issue < IssueBasic include ::API::Helpers::RelatedResourcesHelpers + expose(:has_tasks) do |issue, _| + !issue.task_list_items.empty? + end + + expose :task_status, if: -> (issue, _) do + !issue.task_list_items.empty? + end + expose :_links do expose :self do |issue| expose_url(api_v4_project_issue_path(id: issue.project_id, issue_iid: issue.iid)) @@ -878,7 +891,7 @@ module API expose :push_event_payload, as: :push_data, using: PushEventPayload, - if: -> (event, _) { event.push? } + if: -> (event, _) { event.push_action? } expose :author_username do |event, options| event.author&.username diff --git a/lib/api/groups.rb b/lib/api/groups.rb index ad16f26f5cc..6893c8c40be 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -20,20 +20,19 @@ module API optional :share_with_group_lock, type: Boolean, desc: 'Prevent sharing a project with another group within this group' end - if Gitlab.ee? - params :optional_params_ee do - optional :membership_lock, type: Boolean, desc: 'Prevent adding new members to project membership within this group' - optional :ldap_cn, type: String, desc: 'LDAP Common Name' - optional :ldap_access, type: Integer, desc: 'A valid access level' - optional :shared_runners_minutes_limit, type: Integer, desc: '(admin-only) Pipeline minutes quota for this group' - optional :extra_shared_runners_minutes_limit, type: Integer, desc: '(admin-only) Extra pipeline minutes quota for this group' - all_or_none_of :ldap_cn, :ldap_access - end + params :optional_params_ee do + end + + params :optional_update_params_ee do end + end + include ::API::Helpers::GroupsHelpers + + helpers do params :optional_params do use :optional_params_ce - use :optional_params_ee if Gitlab.ee? + use :optional_params_ee end params :statistics_params do @@ -176,10 +175,7 @@ module API optional :name, type: String, desc: 'The name of the group' optional :path, type: String, desc: 'The path of the group' use :optional_params - - if Gitlab.ee? - optional :file_template_project_id, type: Integer, desc: 'The ID of a project to use for custom templates in this group' - end + use :optional_update_params_ee end put ':id' do group = find_group!(params[:id]) diff --git a/lib/api/helpers/groups_helpers.rb b/lib/api/helpers/groups_helpers.rb new file mode 100644 index 00000000000..ae677547760 --- /dev/null +++ b/lib/api/helpers/groups_helpers.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Helpers + module GroupsHelpers + extend ActiveSupport::Concern + end + end +end diff --git a/lib/api/helpers/issues_helpers.rb b/lib/api/helpers/issues_helpers.rb index f6762910b0c..fc66cec5341 100644 --- a/lib/api/helpers/issues_helpers.rb +++ b/lib/api/helpers/issues_helpers.rb @@ -18,6 +18,39 @@ module API :title ] end + + def issue_finder(args = {}) + args = declared_params.merge(args) + + args.delete(:id) + args[:milestone_title] ||= args.delete(:milestone) + args[:label_name] ||= args.delete(:labels) + args[:scope] = args[:scope].underscore if args[:scope] + + IssuesFinder.new(current_user, args) + end + + def find_issues(args = {}) + finder = issue_finder(args) + issues = finder.execute.with_api_entity_associations + + issues.reorder(order_options_with_tie_breaker) # rubocop: disable CodeReuse/ActiveRecord + end + + def issues_statistics(args = {}) + finder = issue_finder(args) + counter = Gitlab::IssuablesCountForState.new(finder) + + { + statistics: { + counts: { + all: counter[:all], + closed: counter[:closed], + opened: counter[:opened] + } + } + } + end end end end diff --git a/lib/api/helpers/related_resources_helpers.rb b/lib/api/helpers/related_resources_helpers.rb index 793ae11b41d..9cdde25fe4e 100644 --- a/lib/api/helpers/related_resources_helpers.rb +++ b/lib/api/helpers/related_resources_helpers.rb @@ -13,6 +13,10 @@ module API available?(:merge_requests, project, options[:current_user]) end + def expose_path(path) + Gitlab::Utils.append_path(Gitlab.config.gitlab.relative_url_root, path) + end + def expose_url(path) url_options = Gitlab::Application.routes.default_url_options protocol, host, port, script_name = url_options.values_at(:protocol, :host, :port, :script_name) diff --git a/lib/api/issues.rb b/lib/api/issues.rb index d0a93b77951..0b4da01f3c8 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -3,27 +3,12 @@ module API class Issues < Grape::API include PaginationParams + helpers Helpers::IssuesHelpers + helpers ::Gitlab::IssuableMetadata before { authenticate_non_get! } - helpers ::Gitlab::IssuableMetadata - helpers do - # rubocop: disable CodeReuse/ActiveRecord - def find_issues(args = {}) - args = declared_params.merge(args) - - args.delete(:id) - args[:milestone_title] = args.delete(:milestone) - args[:label_name] = args.delete(:labels) - args[:scope] = args[:scope].underscore if args[:scope] - - issues = IssuesFinder.new(current_user, args).execute - .with_api_entity_associations - issues.reorder(order_options_with_tie_breaker) - end - # rubocop: enable CodeReuse/ActiveRecord - if Gitlab.ee? params :issues_params_ee do optional :weight, types: [Integer, String], integer_none_any: true, desc: 'The weight of the issue' @@ -34,13 +19,9 @@ module API end end - params :issues_params do + params :issues_stats_params do optional :labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names' optional :milestone, type: String, desc: 'Milestone title' - optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at', - desc: 'Return issues ordered by `created_at` or `updated_at` fields.' - optional :sort, type: String, values: %w[asc desc], default: 'desc', - desc: 'Return issues sorted in `asc` or `desc` order.' optional :milestone, type: String, desc: 'Return issues for a specific milestone' optional :iids, type: Array[Integer], desc: 'The IID array of issues' optional :search, type: String, desc: 'Search issues for text present in the title, description, or any combination of these' @@ -49,18 +30,39 @@ module API optional :created_before, type: DateTime, desc: 'Return issues created before the specified time' optional :updated_after, type: DateTime, desc: 'Return issues updated after the specified time' optional :updated_before, type: DateTime, desc: 'Return issues updated before the specified time' + optional :author_id, type: Integer, desc: 'Return issues which are authored by the user with the given ID' + optional :author_username, type: String, desc: 'Return issues which are authored by the user with the given username' + mutually_exclusive :author_id, :author_username + optional :assignee_id, types: [Integer, String], integer_none_any: true, desc: 'Return issues which are assigned to the user with the given ID' + optional :assignee_username, type: Array[String], check_assignees_count: true, + coerce_with: Validations::CheckAssigneesCount.coerce, + desc: 'Return issues which are assigned to the user with the given username' + mutually_exclusive :assignee_id, :assignee_username + optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all], desc: 'Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`' optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji' optional :confidential, type: Boolean, desc: 'Filter confidential or public issues' - use :pagination use :issues_params_ee if Gitlab.ee? end + params :issues_params do + optional :with_labels_details, type: Boolean, desc: 'Return more label data than just lable title', default: false + optional :state, type: String, values: %w[opened closed all], default: 'all', + desc: 'Return opened, closed, or all issues' + optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at', + desc: 'Return issues ordered by `created_at` or `updated_at` fields.' + optional :sort, type: String, values: %w[asc desc], default: 'desc', + desc: 'Return issues sorted in `asc` or `desc` order.' + + use :issues_stats_params + use :pagination + end + params :issue_params do optional :description, type: String, desc: 'The description of an issue' optional :assignee_ids, type: Array[Integer], desc: 'The array of user IDs to assign issue' @@ -75,13 +77,23 @@ module API end end + desc "Get currently authenticated user's issues statistics" + params do + use :issues_stats_params + optional :scope, type: String, values: %w[created_by_me assigned_to_me all], default: 'created_by_me', + desc: 'Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`' + end + get '/issues_statistics' do + authenticate! unless params[:scope] == 'all' + + present issues_statistics, with: Grape::Presenters::Presenter + end + resource :issues do desc "Get currently authenticated user's issues" do - success Entities::IssueBasic + success Entities::Issue end params do - optional :state, type: String, values: %w[opened closed all], default: 'all', - desc: 'Return opened, closed, or all issues' use :issues_params optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all], default: 'created_by_me', desc: 'Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`' @@ -91,7 +103,8 @@ module API issues = paginate(find_issues) options = { - with: Entities::IssueBasic, + with: Entities::Issue, + with_labels_details: declared_params[:with_labels_details], current_user: current_user, issuable_metadata: issuable_meta_data(issues, 'Issue') } @@ -105,11 +118,9 @@ module API end resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Get a list of group issues' do - success Entities::IssueBasic + success Entities::Issue end params do - optional :state, type: String, values: %w[opened closed all], default: 'all', - desc: 'Return opened, closed, or all issues' use :issues_params end get ":id/issues" do @@ -118,13 +129,24 @@ module API issues = paginate(find_issues(group_id: group.id, include_subgroups: true)) options = { - with: Entities::IssueBasic, + with: Entities::Issue, + with_labels_details: declared_params[:with_labels_details], current_user: current_user, issuable_metadata: issuable_meta_data(issues, 'Issue') } present issues, options end + + desc 'Get statistics for the list of group issues' + params do + use :issues_stats_params + end + get ":id/issues_statistics" do + group = find_group!(params[:id]) + + present issues_statistics(group_id: group.id, include_subgroups: true), with: Grape::Presenters::Presenter + end end params do @@ -134,11 +156,9 @@ module API include TimeTrackingEndpoints desc 'Get a list of project issues' do - success Entities::IssueBasic + success Entities::Issue end params do - optional :state, type: String, values: %w[opened closed all], default: 'all', - desc: 'Return opened, closed, or all issues' use :issues_params end get ":id/issues" do @@ -147,7 +167,8 @@ module API issues = paginate(find_issues(project_id: project.id)) options = { - with: Entities::IssueBasic, + with: Entities::Issue, + with_labels_details: declared_params[:with_labels_details], current_user: current_user, project: user_project, issuable_metadata: issuable_meta_data(issues, 'Issue') @@ -156,6 +177,16 @@ module API present issues, options end + desc 'Get statistics for the list of project issues' + params do + use :issues_stats_params + end + get ":id/issues_statistics" do + project = find_project!(params[:id]) + + present issues_statistics(project_id: project.id), with: Grape::Presenters::Presenter + end + desc 'Get a single project issue' do success Entities::Issue end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index ce85772e4ed..daa98c22e5e 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -367,6 +367,10 @@ module API merge_request = find_project_merge_request(params[:merge_request_iid]) merge_when_pipeline_succeeds = to_boolean(params[:merge_when_pipeline_succeeds]) + if merge_when_pipeline_succeeds || merge_request.merge_when_pipeline_succeeds + render_api_error!('Not allowed: pipeline does not exist', 405) unless merge_request.head_pipeline + end + # Merge request can not be merged # because user dont have permissions to push into target branch unauthorized! unless merge_request.can_be_merged_by?(current_user) diff --git a/lib/api/runner.rb b/lib/api/runner.rb index ea36c24eca2..fdf4904e9f5 100644 --- a/lib/api/runner.rb +++ b/lib/api/runner.rb @@ -98,6 +98,7 @@ module API optional :certificate, type: String, desc: %q(Session's certificate) optional :authorization, type: String, desc: %q(Session's authorization) end + optional :job_age, type: Integer, desc: %q(Job should be older than passed age in seconds to be ran on runner) end post '/request' do authenticate_runner! diff --git a/lib/api/search.rb b/lib/api/search.rb index 60095300ea1..1cab1a97186 100644 --- a/lib/api/search.rb +++ b/lib/api/search.rb @@ -112,12 +112,13 @@ module API type: String, desc: 'The scope of the search', values: Helpers::SearchHelpers.project_search_scopes + optional :ref, type: String, desc: 'The name of a repository branch or tag. If not given, the default branch is used' use :pagination end get ':id/(-/)search' do check_users_search_allowed! - present search(project_id: user_project.id), with: entity + present search({ project_id: user_project.id, repository_ref: params[:ref] }), with: entity end end end diff --git a/lib/api/validations/check_assignees_count.rb b/lib/api/validations/check_assignees_count.rb new file mode 100644 index 00000000000..836ec936b31 --- /dev/null +++ b/lib/api/validations/check_assignees_count.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module API + module Validations + class CheckAssigneesCount < Grape::Validations::Base + def self.coerce + lambda do |value| + case value + when String, Array + Array.wrap(value) + else + [] + end + end + end + + def validate_param!(attr_name, params) + return if param_allowed?(attr_name, params) + + raise Grape::Exceptions::Validation, + params: [@scope.full_name(attr_name)], + message: "allows one value, but found #{params[attr_name].size}: #{params[attr_name].join(", ")}" + end + + private + + def param_allowed?(attr_name, params) + params[attr_name].size <= 1 + end + end + end +end diff --git a/lib/bitbucket_server/representation/repo.rb b/lib/bitbucket_server/representation/repo.rb index 6c494b79166..dab7f8f22a1 100644 --- a/lib/bitbucket_server/representation/repo.rb +++ b/lib/bitbucket_server/representation/repo.rb @@ -20,7 +20,7 @@ module BitbucketServer end def browse_url - # The JSON reponse contains an array of 1 element. Not sure if there + # The JSON response contains an array of 1 element. Not sure if there # are cases where multiple links would be provided. raw.dig('links', 'self').first.fetch('href') end diff --git a/lib/gitlab.rb b/lib/gitlab.rb index d301efc3205..3f107fbbf3b 100644 --- a/lib/gitlab.rb +++ b/lib/gitlab.rb @@ -59,7 +59,11 @@ module Gitlab end def self.ee? - Object.const_defined?(:License) + if ENV['IS_GITLAB_EE'].present? + Gitlab::Utils.to_boolean(ENV['IS_GITLAB_EE']) + else + Object.const_defined?(:License) + end end def self.process_name diff --git a/lib/gitlab/background_migration/fix_cross_project_label_links.rb b/lib/gitlab/background_migration/fix_cross_project_label_links.rb index 0a12401c35f..bf5d7f5f322 100644 --- a/lib/gitlab/background_migration/fix_cross_project_label_links.rb +++ b/lib/gitlab/background_migration/fix_cross_project_label_links.rb @@ -95,7 +95,7 @@ module Gitlab local_labels = available_labels(project_id) # get all label links for the given resource (issue/MR) - # which reference a label not included in avaiable_labels + # which reference a label not included in available_labels # (other than its project labels and labels of ancestor groups) cross_labels = LabelLink .select('label_id, labels.title as title, labels.color as color, label_links.id as label_link_id') diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index 769d3279f91..c9f0ed66a54 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -135,7 +135,7 @@ module Gitlab def create_labels LABELS.each do |label_params| - label = ::Labels::CreateService.new(label_params).execute(project: project) + label = ::Labels::FindOrCreateService.new(nil, project, label_params).execute(skip_authorization: true) if label.valid? @labels[label_params[:title]] = label else diff --git a/lib/gitlab/ci/pipeline/chain/limit/activity.rb b/lib/gitlab/ci/pipeline/chain/limit/activity.rb index fe7c8738cc0..68482cf08a9 100644 --- a/lib/gitlab/ci/pipeline/chain/limit/activity.rb +++ b/lib/gitlab/ci/pipeline/chain/limit/activity.rb @@ -7,11 +7,11 @@ module Gitlab module Limit class Activity < Chain::Base def perform! - # to be overriden in EE + # to be overridden in EE end def break? - false # to be overriden in EE + false # to be overridden in EE end end end diff --git a/lib/gitlab/ci/pipeline/chain/limit/size.rb b/lib/gitlab/ci/pipeline/chain/limit/size.rb index b4d51437cd6..cd330c58406 100644 --- a/lib/gitlab/ci/pipeline/chain/limit/size.rb +++ b/lib/gitlab/ci/pipeline/chain/limit/size.rb @@ -7,11 +7,11 @@ module Gitlab module Limit class Size < Chain::Base def perform! - # to be overriden in EE + # to be overridden in EE end def break? - false # to be overriden in EE + false # to be overridden in EE end end end diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml index 9d99d04d263..779f4b5f006 100644 --- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml @@ -338,7 +338,6 @@ rollout 100%: image_tag=${CI_APPLICATION_TAG:-$CI_COMMIT_TAG} fi - replicas="1" service_enabled="true" postgres_enabled="$POSTGRES_ENABLED" @@ -383,7 +382,7 @@ rollout 100%: --set application.database_url="$DATABASE_URL" \ --set application.secretName="$APPLICATION_SECRET_NAME" \ --set application.secretChecksum="$APPLICATION_SECRET_CHECKSUM" \ - --set service.commonName="le.$KUBE_INGRESS_BASE_DOMAIN" \ + --set service.commonName="le-$CI_PROJECT_ID.$KUBE_INGRESS_BASE_DOMAIN" \ --set service.url="$CI_ENVIRONMENT_URL" \ --set service.additionalHosts="$additional_hosts" \ --set replicaCount="$replicas" \ @@ -424,7 +423,7 @@ rollout 100%: --set application.database_url="$DATABASE_URL" \ --set application.secretName="$APPLICATION_SECRET_NAME" \ --set application.secretChecksum="$APPLICATION_SECRET_CHECKSUM" \ - --set service.commonName="le.$KUBE_INGRESS_BASE_DOMAIN" \ + --set service.commonName="le-$CI_PROJECT_ID.$KUBE_INGRESS_BASE_DOMAIN" \ --set service.url="$CI_ENVIRONMENT_URL" \ --set service.additionalHosts="$additional_hosts" \ --set replicaCount="$replicas" \ @@ -440,7 +439,9 @@ rollout 100%: chart/ fi - kubectl rollout status -n "$KUBE_NAMESPACE" -w "$ROLLOUT_RESOURCE_TYPE/$name" + if [[ -z "$ROLLOUT_STATUS_DISABLED" ]]; then + kubectl rollout status -n "$KUBE_NAMESPACE" -w "$ROLLOUT_RESOURCE_TYPE/$name" + fi } function scale() { diff --git a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml index eef361c19e9..324e39c7747 100644 --- a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml @@ -22,7 +22,7 @@ container_scanning: DOCKER_SERVICE: docker DOCKER_HOST: tcp://${DOCKER_SERVICE}:2375/ # https://hub.docker.com/r/arminc/clair-local-scan/tags - CLAIR_LOCAL_SCAN_VERSION: v2.0.6 + CLAIR_LOCAL_SCAN_VERSION: v2.0.8_fe9b059d930314b54c78f75afe265955faf4fdc1 allow_failure: true services: - docker:stable-dind diff --git a/lib/gitlab/ci/templates/dotNET-Core.yml b/lib/gitlab/ci/templates/dotNET-Core.yml index 558ca3d22e1..708b75f83e8 100644 --- a/lib/gitlab/ci/templates/dotNET-Core.yml +++ b/lib/gitlab/ci/templates/dotNET-Core.yml @@ -3,10 +3,11 @@ # ### Specify the Docker image # -# Instead of installing .NET Core SDK manually, a docker image is used -# with already pre-installed .NET Core SDK. -# The 'latest' tag targets the latest available version of .NET Core SDK image. -# If preferred, you can explicitly specify version of .NET Core e.g. using '2.2-sdk' tag. +# Instead of installing .NET Core SDK manually, a docker image is used +# with already pre-installed .NET Core SDK. +# +# The 'latest' tag targets the latest available version of .NET Core SDK image. +# If preferred, you can explicitly specify version of .NET Core (e.g. using '2.2-sdk' tag). # # See other available tags for .NET Core: https://hub.docker.com/r/microsoft/dotnet # Learn more about Docker tags: https://docs.docker.com/glossary/?term=tag @@ -17,16 +18,16 @@ image: microsoft/dotnet:latest # variables: # 1) Name of directory where restore and build objects are stored. - OBJECTS_DIRECTORY: 'obj' - # 2) Name of directory used for keeping restored dependencies. + OBJECTS_DIRECTORY: 'obj' + # 2) Name of directory used for keeping restored dependencies. NUGET_PACKAGES_DIRECTORY: '.nuget' # 3) A relative path to the source code from project repository root. # NOTE: Please edit this path so it matches the structure of your project! - SOURCE_CODE_PATH: '*/*/' + SOURCE_CODE_PATH: '*/*/' # ### Define stage list # -# In this example there are only two stages. +# In this example there are only two stages. # Initially, the project will be built and then tested. stages: - build @@ -34,47 +35,55 @@ stages: # ### Define global cache rule # -# Before building the project, all dependencies (e.g. third-party NuGet packages) -# must be restored. Jobs on GitLab.com's Shared Runners are executed on autoscaled machines. -# Each machine is used only once (for security reasons) and after that it is removed. -# What that means is that before every job a dependency restore must be performed +# Before building the project, all dependencies (e.g. third-party NuGet packages) +# must be restored. Jobs on GitLab.com's Shared Runners are executed on autoscaled machines. +# +# Each machine is used only once (for security reasons) and after that is removed. +# This means that, before every job, a dependency restore must be performed # because restored dependencies are removed along with machines. Fortunately, # GitLab provides cache mechanism with the aim of keeping restored dependencies -# for other jobs. This example shows how to configure cache to pass over restored +# for other jobs. +# +# This example shows how to configure cache to pass over restored # dependencies for re-use. # # With global cache rule, cached dependencies will be downloaded before every job # and then unpacked to the paths as specified below. cache: # Per-stage and per-branch caching. - key: "$CI_JOB_STAGE-$CI_COMMIT_REF_SLUG" + key: "$CI_JOB_STAGE-$CI_COMMIT_REF_SLUG" paths: # Specify three paths that should be cached: # # 1) Main JSON file holding information about package dependency tree, packages versions, # frameworks etc. It also holds information where to the dependencies were restored. - - '$SOURCE_CODE_PATH$OBJECTS_DIRECTORY/project.assets.json' + - '$SOURCE_CODE_PATH$OBJECTS_DIRECTORY/project.assets.json' # 2) Other NuGet and MSBuild related files. Also needed. - - '$SOURCE_CODE_PATH$OBJECTS_DIRECTORY/*.csproj.nuget.*' + - '$SOURCE_CODE_PATH$OBJECTS_DIRECTORY/*.csproj.nuget.*' # 3) Path to the directory where restored dependencies are kept. - - '$NUGET_PACKAGES_DIRECTORY' - # 'pull-push' policy means that latest cache will be downloaded (if exists) - # before executing the job, and a newer version will be uploaded afterwards. - # Such setting saves time when there are no changes in referenced third-party - # packages. For example if you run a pipeline with changes in your code, - # but with no changes within third-party packages which your project is using, - # then project restore will happen in next to no time as all required dependencies - # will already be there — unzipped from cache. 'pull-push' policy is a default - # cache policy, you do not have to specify it explicitly. - policy: pull-push + - '$NUGET_PACKAGES_DIRECTORY' + # + # 'pull-push' policy means that latest cache will be downloaded (if it exists) + # before executing the job, and a newer version will be uploaded afterwards. + # Such a setting saves time when there are no changes in referenced third-party + # packages. + # + # For example, if you run a pipeline with changes in your code, + # but with no changes within third-party packages which your project is using, + # then project restore will happen quickly as all required dependencies + # will already be there — unzipped from cache. + + # 'pull-push' policy is the default cache policy, you do not have to specify it explicitly. + policy: pull-push # ### Restore project dependencies # -# NuGet packages by default are restored to '.nuget/packages' directory -# in the user's home directory. That directory is out of scope of GitLab caching. -# To get around this a custom path can be specified using '--packages <PATH>' option -# for 'dotnet restore' command. In this example a temporary directory is created -# in the root of project repository, so it's content can be cached. +# NuGet packages by default are restored to '.nuget/packages' directory +# in the user's home directory. That directory is out of scope of GitLab caching. +# +# To get around this, a custom path can be specified using the '--packages <PATH>' option +# for 'dotnet restore' command. In this example, a temporary directory is created +# in the root of project repository, so its content can be cached. # # Learn more about GitLab cache: https://docs.gitlab.com/ee/ci/caching/index.html before_script: @@ -82,26 +91,26 @@ before_script: build: stage: build - # ### Build all projects discovered from solution file. + # ### Build all projects discovered from solution file. # - # Note: this will fail if you have any projects in your solution that are not - # .NET Core based projects e.g. WCF service, which is based on .NET Framework, - # not .NET Core. In such scenario you will need to build every .NET Core based - # project by explicitly specifying a relative path to the directory - # where it is located e.g. 'dotnet build ./src/ConsoleApp'. + # Note: this will fail if you have any projects in your solution that are not + # .NET Core-based projects (e.g. WCF service), which is based on .NET Framework, + # not .NET Core. In this scenario, you will need to build every .NET Core-based + # project by explicitly specifying a relative path to the directory + # where it is located (e.g. 'dotnet build ./src/ConsoleApp'). # Only one project path can be passed as a parameter to 'dotnet build' command. script: - - 'dotnet build --no-restore' + - 'dotnet build --no-restore' tests: stage: test # ### Run the tests # - # You can either run tests for all test projects that are defined in your solution + # You can either run tests for all test projects that are defined in your solution # with 'dotnet test' or run tests only for specific project by specifying - # a relative path to the directory where it is located e.g. 'dotnet test ./test/UnitTests'. + # a relative path to the directory where it is located (e.g. 'dotnet test ./test/UnitTests'). # # You may want to define separate testing jobs for different types of testing - # e.g. integration tests, unit tests etc. + # (e.g. integration tests, unit tests etc). script: - 'dotnet test --no-restore' diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb index df34d254c65..6796fcce75f 100644 --- a/lib/gitlab/config/entry/validators.rb +++ b/lib/gitlab/config/entry/validators.rb @@ -36,10 +36,10 @@ module Gitlab class AllowedArrayValuesValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) - unkown_values = value - options[:in] - unless unkown_values.empty? + unknown_values = value - options[:in] + unless unknown_values.empty? record.errors.add(attribute, "contains unknown values: " + - unkown_values.join(', ')) + unknown_values.join(', ')) end end end diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb index 3ef19d801b7..f0ca397609d 100644 --- a/lib/gitlab/danger/helper.rb +++ b/lib/gitlab/danger/helper.rb @@ -121,6 +121,8 @@ module Gitlab \.prettierrc | \.scss-lint.yml | \.stylelintrc | + \.haml-lint.yml | + \.haml-lint_todo.yml | babel\.config\.js | jest\.config\.js | karma\.config\.js | diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 7f5eb1188fc..cc61bb7fa02 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -905,6 +905,12 @@ module Gitlab end end + def remove_foreign_key_if_exists(*args) + if foreign_key_exists?(*args) + remove_foreign_key(*args) + end + end + def remove_foreign_key_without_error(*args) remove_foreign_key(*args) rescue ArgumentError diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index fc9bcbdcca2..455588f3c66 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -500,7 +500,7 @@ module Gitlab end # Return total diverging commits count - def diverging_commit_count(from, to, max_count:) + def diverging_commit_count(from, to, max_count: 0) wrapped_gitaly_errors do gitaly_commit_client.diverging_commit_count(from, to, max_count: max_count) end diff --git a/lib/gitlab/git/rugged_impl/tree.rb b/lib/gitlab/git/rugged_impl/tree.rb index bb13d114d46..9c37bb01961 100644 --- a/lib/gitlab/git/rugged_impl/tree.rb +++ b/lib/gitlab/git/rugged_impl/tree.rb @@ -43,6 +43,8 @@ module Gitlab ordered_entries.concat(tree_entries_from_rugged(repository, sha, entry.path, true)) end end + + ordered_entries end def rugged_populate_flat_path(repository, sha, path, entries) diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index 4908f236cd1..05e06eec012 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -32,7 +32,8 @@ module Gitlab CLIENT_NAME = (Sidekiq.server? ? 'gitlab-sidekiq' : 'gitlab-web').freeze SERVER_FEATURE_CATFILE_CACHE = 'catfile-cache'.freeze - SERVER_FEATURE_FLAGS = [SERVER_FEATURE_CATFILE_CACHE].freeze + # Server feature flags should use '_' to separate words. + SERVER_FEATURE_FLAGS = [SERVER_FEATURE_CATFILE_CACHE, 'delta_islands'].freeze MUTEX = Mutex.new diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index 2896b7e1ce0..d21b98d36ea 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -79,7 +79,7 @@ module Gitlab def tree_entry(ref, path, limit = nil) if Pathname.new(path).cleanpath.to_s.start_with?('../') - # The TreeEntry RPC should return an empty reponse in this case but in + # The TreeEntry RPC should return an empty response in this case but in # Gitaly 0.107.0 and earlier we get an exception instead. This early return # saves us a Gitaly roundtrip while also avoiding the exception. return diff --git a/lib/gitlab/group_search_results.rb b/lib/gitlab/group_search_results.rb index 7255293b194..334642f252e 100644 --- a/lib/gitlab/group_search_results.rb +++ b/lib/gitlab/group_search_results.rb @@ -2,6 +2,8 @@ module Gitlab class GroupSearchResults < SearchResults + attr_reader :group + def initialize(current_user, limit_projects, group, query, default_project_filter: false, per_page: 20) super(current_user, limit_projects, query, default_project_filter: default_project_filter, per_page: per_page) @@ -26,5 +28,9 @@ module Gitlab .where(id: groups.select('members.user_id')) end # rubocop:enable CodeReuse/ActiveRecord + + def issuable_params + super.merge(group_id: group.id) + end end end diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index c6d4fda4af5..7bbcb53f016 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -176,6 +176,8 @@ excluded_attributes: - :enabled methods: + notes: + - :type labels: - :type label: diff --git a/lib/gitlab/lets_encrypt/challenge.rb b/lib/gitlab/lets_encrypt/challenge.rb new file mode 100644 index 00000000000..6a7f5e965c5 --- /dev/null +++ b/lib/gitlab/lets_encrypt/challenge.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Gitlab + module LetsEncrypt + class Challenge + def initialize(acme_challenge) + @acme_challenge = acme_challenge + end + + delegate :url, :token, :file_content, :status, :request_validation, to: :acme_challenge + + private + + attr_reader :acme_challenge + end + end +end diff --git a/lib/gitlab/lets_encrypt/client.rb b/lib/gitlab/lets_encrypt/client.rb new file mode 100644 index 00000000000..d7468b06767 --- /dev/null +++ b/lib/gitlab/lets_encrypt/client.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +module Gitlab + module LetsEncrypt + class Client + PRODUCTION_DIRECTORY_URL = 'https://acme-v02.api.letsencrypt.org/directory' + STAGING_DIRECTORY_URL = 'https://acme-staging-v02.api.letsencrypt.org/directory' + + def new_order(domain_name) + ensure_account + + acme_order = acme_client.new_order(identifiers: [domain_name]) + + ::Gitlab::LetsEncrypt::Order.new(acme_order) + end + + def load_order(url) + ensure_account + + # rubocop: disable CodeReuse/ActiveRecord + ::Gitlab::LetsEncrypt::Order.new(acme_client.order(url: url)) + # rubocop: enable CodeReuse/ActiveRecord + end + + def load_challenge(url) + ensure_account + + ::Gitlab::LetsEncrypt::Challenge.new(acme_client.challenge(url: url)) + end + + def terms_of_service_url + acme_client.terms_of_service + end + + def enabled? + return false unless Feature.enabled?(:pages_auto_ssl) + + Gitlab::CurrentSettings.lets_encrypt_terms_of_service_accepted + end + + private + + def acme_client + @acme_client ||= ::Acme::Client.new(private_key: private_key, directory: acme_api_directory_url) + end + + def private_key + @private_key ||= OpenSSL::PKey.read(Gitlab::Application.secrets.lets_encrypt_private_key) + end + + def admin_email + Gitlab::CurrentSettings.lets_encrypt_notification_email + end + + def contact + "mailto:#{admin_email}" + end + + def ensure_account + raise 'Acme integration is disabled' unless enabled? + + @acme_account ||= acme_client.new_account(contact: contact, terms_of_service_agreed: true) + end + + def acme_api_directory_url + if Rails.env.production? + PRODUCTION_DIRECTORY_URL + else + STAGING_DIRECTORY_URL + end + end + end + end +end diff --git a/lib/gitlab/lets_encrypt/order.rb b/lib/gitlab/lets_encrypt/order.rb new file mode 100644 index 00000000000..5109b5e9843 --- /dev/null +++ b/lib/gitlab/lets_encrypt/order.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module LetsEncrypt + class Order + def initialize(acme_order) + @acme_order = acme_order + end + + def new_challenge + authorization = @acme_order.authorizations.first + challenge = authorization.http + ::Gitlab::LetsEncrypt::Challenge.new(challenge) + end + + delegate :url, :status, to: :acme_order + + private + + attr_reader :acme_order + end + end +end diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb index a07b1246bee..aa2c1ac9cef 100644 --- a/lib/gitlab/path_regex.rb +++ b/lib/gitlab/path_regex.rb @@ -58,6 +58,7 @@ module Gitlab uploads users v2 + visual-review-toolbar.js ].freeze # This list should contain all words following `/*namespace_id/:project_id` in diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index 58f06b6708c..78337518988 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -145,5 +145,9 @@ module Gitlab def repository_wiki_ref @repository_wiki_ref ||= repository_ref || project.wiki.default_branch end + + def issuable_params + super.merge(project_id: project.id) + end end end diff --git a/lib/gitlab/routing.rb b/lib/gitlab/routing.rb index 3b05f181ed2..84885be9bda 100644 --- a/lib/gitlab/routing.rb +++ b/lib/gitlab/routing.rb @@ -45,7 +45,7 @@ module Gitlab def self.redirect_legacy_paths(router, *paths) build_redirect_path = lambda do |request, _params, path| - # Only replace the last occurence of `path`. + # Only replace the last occurrence of `path`. # # `request.fullpath` includes the querystring new_path = request.path.sub(%r{/#{path}(/*)(?!.*#{path})}, "/-/#{path}\\1") diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb index a29517e068f..4a097a00101 100644 --- a/lib/gitlab/search_results.rb +++ b/lib/gitlab/search_results.rb @@ -2,6 +2,8 @@ module Gitlab class SearchResults + COUNT_LIMIT = 1001 + attr_reader :current_user, :query, :per_page # Limit search results by passed projects @@ -25,29 +27,26 @@ module Gitlab def objects(scope, page = nil, without_count = true) collection = case scope when 'projects' - projects.page(page).per(per_page) + projects when 'issues' - issues.page(page).per(per_page) + issues when 'merge_requests' - merge_requests.page(page).per(per_page) + merge_requests when 'milestones' - milestones.page(page).per(per_page) + milestones when 'users' - users.page(page).per(per_page) + users else - Kaminari.paginate_array([]).page(page).per(per_page) - end + Kaminari.paginate_array([]) + end.page(page).per(per_page) without_count ? collection.without_count : collection end - # rubocop: disable CodeReuse/ActiveRecord def limited_projects_count - @limited_projects_count ||= projects.limit(count_limit).count + @limited_projects_count ||= limited_count(projects) end - # rubocop: enable CodeReuse/ActiveRecord - # rubocop: disable CodeReuse/ActiveRecord def limited_issues_count return @limited_issues_count if @limited_issues_count @@ -56,35 +55,28 @@ module Gitlab # and confidential issues user has access to, is too complex. # It's faster to try to fetch all public issues first, then only # if necessary try to fetch all issues. - sum = issues(public_only: true).limit(count_limit).count - @limited_issues_count = sum < count_limit ? issues.limit(count_limit).count : sum + sum = limited_count(issues(public_only: true)) + @limited_issues_count = sum < count_limit ? limited_count(issues) : sum end - # rubocop: enable CodeReuse/ActiveRecord - # rubocop: disable CodeReuse/ActiveRecord def limited_merge_requests_count - @limited_merge_requests_count ||= merge_requests.limit(count_limit).count + @limited_merge_requests_count ||= limited_count(merge_requests) end - # rubocop: enable CodeReuse/ActiveRecord - # rubocop: disable CodeReuse/ActiveRecord def limited_milestones_count - @limited_milestones_count ||= milestones.limit(count_limit).count + @limited_milestones_count ||= limited_count(milestones) end - # rubocop: enable CodeReuse/ActiveRecord - # rubocop:disable CodeReuse/ActiveRecord def limited_users_count - @limited_users_count ||= users.limit(count_limit).count + @limited_users_count ||= limited_count(users) end - # rubocop:enable CodeReuse/ActiveRecord def single_commit_result? false end def count_limit - 1001 + COUNT_LIMIT end def users @@ -99,23 +91,15 @@ module Gitlab limit_projects.search(query) end - # rubocop: disable CodeReuse/ActiveRecord def issues(finder_params = {}) - issues = IssuesFinder.new(current_user, finder_params).execute + issues = IssuesFinder.new(current_user, issuable_params.merge(finder_params)).execute + unless default_project_filter - issues = issues.where(project_id: project_ids_relation) + issues = issues.where(project_id: project_ids_relation) # rubocop: disable CodeReuse/ActiveRecord end - issues = - if query =~ /#(\d+)\z/ - issues.where(iid: $1) - else - issues.full_search(query) - end - - issues.reorder('issues.updated_at DESC') + issues end - # rubocop: enable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord def milestones @@ -125,23 +109,15 @@ module Gitlab end # rubocop: enable CodeReuse/ActiveRecord - # rubocop: disable CodeReuse/ActiveRecord def merge_requests - merge_requests = MergeRequestsFinder.new(current_user).execute + merge_requests = MergeRequestsFinder.new(current_user, issuable_params).execute + unless default_project_filter merge_requests = merge_requests.in_projects(project_ids_relation) end - merge_requests = - if query =~ /[#!](\d+)\z/ - merge_requests.where(iid: $1) - else - merge_requests.full_search(query) - end - - merge_requests.reorder('merge_requests.updated_at DESC') + merge_requests end - # rubocop: enable CodeReuse/ActiveRecord def default_scope 'projects' @@ -152,5 +128,23 @@ module Gitlab limit_projects.select(:id).reorder(nil) end # rubocop: enable CodeReuse/ActiveRecord + + def issuable_params + {}.tap do |params| + params[:sort] = 'updated_desc' + + if query =~ /#(\d+)\z/ + params[:iids] = $1 + else + params[:search] = query + end + end + end + + # rubocop: disable CodeReuse/ActiveRecord + def limited_count(relation) + relation.reorder(nil).limit(count_limit).size + end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/lib/gitlab/sentry.rb b/lib/gitlab/sentry.rb index 356e6445e0e..72c44114001 100644 --- a/lib/gitlab/sentry.rb +++ b/lib/gitlab/sentry.rb @@ -10,7 +10,7 @@ module Gitlab def self.context(current_user = nil) return unless enabled? - Raven.tags_context(locale: I18n.locale) + Raven.tags_context(default_tags) if current_user Raven.user_context( @@ -44,16 +44,19 @@ module Gitlab extra[:issue_url] = issue_url if issue_url context # Make sure we've set everything we know in the context - tags = { - Labkit::Correlation::CorrelationId::LOG_KEY.to_sym => Labkit::Correlation::CorrelationId.current_id - } - - Raven.capture_exception(exception, tags: tags, extra: extra) + Raven.capture_exception(exception, tags: default_tags, extra: extra) end end def self.should_raise_for_dev? Rails.env.development? || Rails.env.test? end + + def self.default_tags + { + Labkit::Correlation::CorrelationId::LOG_KEY.to_sym => Labkit::Correlation::CorrelationId.current_id, + locale: I18n.locale + } + end end end diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 08156d7ffa6..9aa2e972adf 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -26,7 +26,7 @@ module Gitlab uuid: Gitlab::CurrentSettings.uuid, hostname: Gitlab.config.gitlab.host, version: Gitlab::VERSION, - installation_type: Gitlab::INSTALLATION_TYPE, + installation_type: installation_type, active_user_count: count(User.active), recorded_at: Time.now, edition: 'CE' @@ -81,6 +81,7 @@ module Gitlab milestone_lists: count(List.milestone), milestones: count(Milestone), pages_domains: count(PagesDomain), + pool_repositories: count(PoolRepository), projects: count(Project), projects_imported_from_github: count(Project.where(import_type: 'github')), projects_with_repositories_enabled: count(ProjectFeature.where('repository_access_level > ?', ProjectFeature::DISABLED)), @@ -190,6 +191,14 @@ module Gitlab result[key] = approx_counts[model] || -1 end end + + def installation_type + if Rails.env.production? + Gitlab::INSTALLATION_TYPE + else + "gitlab-development-kit" + end + end end end end diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb index e2083848a8d..722e3e04d1c 100644 --- a/lib/mattermost/session.rb +++ b/lib/mattermost/session.rb @@ -122,7 +122,7 @@ module Mattermost @oauth_uri = nil - response = get('/oauth/gitlab/login', follow_redirects: false, format: 'text/html') + response = get('/oauth/gitlab/login', follow_redirects: false) return unless (300...400) === response.code redirect_uri = response.headers['location'] diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab index 2f2de083dc0..32df74f104a 100755 --- a/lib/support/init.d/gitlab +++ b/lib/support/init.d/gitlab @@ -1,8 +1,8 @@ #! /bin/sh # GITLAB -# Maintainer: @randx -# Authors: rovanion.luckey@gmail.com, @randx +# Maintainer: @dzaporozhets +# Authors: rovanion.luckey@gmail.com, @dzaporozhets ### BEGIN INIT INFO # Provides: gitlab @@ -26,6 +26,7 @@ ### Environment variables RAILS_ENV="production" +EXPERIMENTAL_PUMA="" # Script variable names should be lower-case not to conflict with # internal /bin/sh variables such as PATH, EDITOR or SHELL. @@ -75,7 +76,7 @@ check_pids(){ echo "Could not create the path $pid_path needed to store the pids." exit 1 fi - # If there exists a file which should hold the value of the Unicorn pid: read it. + # If there exists a file which should hold the value of the web server pid: read it. if [ -f "$web_server_pid_path" ]; then wpid=$(cat "$web_server_pid_path") else @@ -198,7 +199,7 @@ check_stale_pids(){ # If there is a pid it is something else than 0, the service is running if # *_status is == 0. if [ "$wpid" != "0" ] && [ "$web_status" != "0" ]; then - echo "Removing stale Unicorn web server pid. This is most likely caused by the web server crashing the last time it ran." + echo "Removing stale web server pid. This is most likely caused by the web server crashing the last time it ran." if ! rm "$web_server_pid_path"; then echo "Unable to remove stale pid, exiting." exit 1 @@ -250,12 +251,12 @@ exit_if_not_running(){ fi } -## Starts Unicorn and Sidekiq if they're not running. +## Starts web server and Sidekiq if they're not running. start_gitlab() { check_stale_pids if [ "$web_status" != "0" ]; then - echo "Starting GitLab Unicorn" + echo "Starting GitLab web server" fi if [ "$sidekiq_status" != "0" ]; then echo "Starting GitLab Sidekiq" @@ -275,12 +276,12 @@ start_gitlab() { # Then check if the service is running. If it is: don't start again. if [ "$web_status" = "0" ]; then - echo "The Unicorn web server already running with pid $wpid, not restarting." + echo "The web server already running with pid $wpid, not restarting." else # Remove old socket if it exists rm -f "$rails_socket" 2>/dev/null # Start the web server - RAILS_ENV=$RAILS_ENV bin/web start + RAILS_ENV=$RAILS_ENV EXPERIMENTAL_PUMA=$EXPERIMENTAL_PUMA bin/web start fi # If sidekiq is already running, don't start it again. @@ -336,13 +337,13 @@ start_gitlab() { print_status } -## Asks Unicorn, Sidekiq and MailRoom if they would be so kind as to stop, if not kills them. +## Asks web server, Sidekiq and MailRoom if they would be so kind as to stop, if not kills them. stop_gitlab() { exit_if_not_running if [ "$web_status" = "0" ]; then - echo "Shutting down GitLab Unicorn" - RAILS_ENV=$RAILS_ENV bin/web stop + echo "Shutting down GitLab web server" + RAILS_ENV=$RAILS_ENV EXPERIMENTAL_PUMA=$EXPERIMENTAL_PUMA bin/web stop fi if [ "$sidekiq_status" = "0" ]; then echo "Shutting down GitLab Sidekiq" @@ -398,9 +399,9 @@ print_status() { return fi if [ "$web_status" = "0" ]; then - echo "The GitLab Unicorn web server with pid $wpid is running." + echo "The GitLab web server with pid $wpid is running." else - printf "The GitLab Unicorn web server is \033[31mnot running\033[0m.\n" + printf "The GitLab web server is \033[31mnot running\033[0m.\n" fi if [ "$sidekiq_status" = "0" ]; then echo "The GitLab Sidekiq job dispatcher with pid $spid is running." @@ -438,15 +439,15 @@ print_status() { fi } -## Tells unicorn to reload its config and Sidekiq to restart +## Tells web server to reload its config and Sidekiq to restart reload_gitlab(){ exit_if_not_running if [ "$wpid" = "0" ];then - echo "The GitLab Unicorn Web server is not running thus its configuration can't be reloaded." + echo "The GitLab web server Web server is not running thus its configuration can't be reloaded." exit 1 fi - printf "Reloading GitLab Unicorn configuration... " - RAILS_ENV=$RAILS_ENV bin/web reload + printf "Reloading GitLab web server configuration... " + RAILS_ENV=$RAILS_ENV EXPERIMENTAL_PUMA=$EXPERIMENTAL_PUMA bin/web reload echo "Done." echo "Restarting GitLab Sidekiq since it isn't capable of reloading its config..." @@ -461,7 +462,7 @@ reload_gitlab(){ print_status } -## Restarts Sidekiq and Unicorn. +## Restarts Sidekiq and web server. restart_gitlab(){ check_status if [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; } || { [ "$gitlab_pages_enabled" = true ] && [ "$gitlab_pages_status" = "0" ]; } || { [ "$gitaly_enabled" = true ] && [ "$gitaly_status" = "0" ]; }; then diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example index 295c79fccfc..ab41dba3017 100644 --- a/lib/support/init.d/gitlab.default.example +++ b/lib/support/init.d/gitlab.default.example @@ -5,6 +5,9 @@ # Normal values are "production", "test" and "development". RAILS_ENV="production" +# Uncomment the line below to enable Puma web server instead of Unicorn. +# EXPERIMENTAL_PUMA=1 + # app_user defines the user that GitLab is run as. # The default is "git". app_user="git" diff --git a/lib/system_check/base_check.rb b/lib/system_check/base_check.rb index 46aad8aa885..c36cacbaf4f 100644 --- a/lib/system_check/base_check.rb +++ b/lib/system_check/base_check.rb @@ -121,7 +121,7 @@ module SystemCheck # # @see #try_fixing_it # @see #fix_and_rerun - # @see #for_more_infromation + # @see #for_more_information def show_error raise NotImplementedError end diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake index 3977fc7ad8c..c531eb1d216 100644 --- a/lib/tasks/gitlab/backup.rake +++ b/lib/tasks/gitlab/backup.rake @@ -20,6 +20,11 @@ namespace :gitlab do backup.pack backup.cleanup backup.remove_old + + puts "Warning: Your gitlab.rb and gitlab-secrets.json files contain sensitive data \n" \ + "and are not included in this backup. You will need these files to restore a backup.\n" \ + "Please back them up manually.".color(:red) + puts "Backup task is done." end # Restore backup of GitLab system @@ -68,6 +73,9 @@ namespace :gitlab do Rake::Task['cache:clear'].invoke backup.cleanup + puts "Warning: Your gitlab.rb and gitlab-secrets.json files contain sensitive data \n" \ + "and are not included in this backup. You will need to restore these files manually.".color(:red) + puts "Restore task is done." end namespace :repo do diff --git a/lib/tasks/gitlab/features.rake b/lib/tasks/gitlab/features.rake index d115961108e..d88bcca0819 100644 --- a/lib/tasks/gitlab/features.rake +++ b/lib/tasks/gitlab/features.rake @@ -17,7 +17,7 @@ namespace :gitlab do if status Feature.enable(flag) else - Feature.disable(flag) + Feature.get(flag).remove end end end diff --git a/lib/tasks/lint.rake b/lib/tasks/lint.rake index c5d0f2c292f..2353b2dc659 100644 --- a/lib/tasks/lint.rake +++ b/lib/tasks/lint.rake @@ -37,32 +37,15 @@ unless Rails.env.production? lint:static_verification ].each do |task| pid = Process.fork do - rd_out, wr_out = IO.pipe - rd_err, wr_err = IO.pipe - stdout = $stdout.dup - stderr = $stderr.dup - $stdout.reopen(wr_out) - $stderr.reopen(wr_err) - - begin - Rake::Task[task].invoke - rescue SystemExit => ex - msg = "*** Rake task #{task} exited:" - raise ex - rescue => ex - msg = "*** Rake task #{task} raised #{ex.class}:" - raise ex - ensure - $stdout.reopen(stdout) - $stderr.reopen(stderr) - wr_out.close - wr_err.close - - warn "\n#{msg}\n\n" if msg - - IO.copy_stream(rd_out, $stdout) - IO.copy_stream(rd_err, $stderr) - end + puts "*** Running rake task: #{task} ***" + + Rake::Task[task].invoke + rescue SystemExit => ex + warn "!!! Rake task #{task} exited:" + raise ex + rescue StandardError, ScriptError => ex + warn "!!! Rake task #{task} raised #{ex.class}:" + raise ex end Process.waitpid(pid) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index cc1a54a5642..c20cae9665a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -754,6 +754,9 @@ msgstr "" msgid "All merge conflicts were resolved. The merge request can now be merged." msgstr "" +msgid "All projects" +msgstr "" + msgid "All todos were marked as done." msgstr "" @@ -865,6 +868,9 @@ msgstr "" msgid "An error occurred while fetching the board lists. Please try again." msgstr "" +msgid "An error occurred while fetching the builds." +msgstr "" + msgid "An error occurred while fetching the job log." msgstr "" @@ -880,6 +886,9 @@ msgstr "" msgid "An error occurred while fetching the releases. Please try again." msgstr "" +msgid "An error occurred while fetching this tab." +msgstr "" + msgid "An error occurred while getting projects" msgstr "" @@ -967,9 +976,15 @@ msgstr "" msgid "Any" msgstr "" +msgid "Any Milestone" +msgstr "" + msgid "Any encrypted tokens" msgstr "" +msgid "Any namespace" +msgstr "" + msgid "Appearance" msgstr "" @@ -1024,6 +1039,9 @@ msgstr "" msgid "Applying multiple commands" msgstr "" +msgid "Applying suggestion" +msgstr "" + msgid "Apr" msgstr "" @@ -1054,6 +1072,12 @@ msgstr "" msgid "Are you sure that you want to unarchive this project?" msgstr "" +msgid "Are you sure you want to cancel creating this comment?" +msgstr "" + +msgid "Are you sure you want to cancel editing this comment?" +msgstr "" + msgid "Are you sure you want to delete this list?" msgstr "" @@ -1888,9 +1912,24 @@ msgstr "" msgid "CiVariables|Input variable value" msgstr "" +msgid "CiVariables|Key" +msgstr "" + +msgid "CiVariables|Masked" +msgstr "" + msgid "CiVariables|Remove variable row" msgstr "" +msgid "CiVariables|State" +msgstr "" + +msgid "CiVariables|Type" +msgstr "" + +msgid "CiVariables|Value" +msgstr "" + msgid "CiVariable|* (All environments)" msgstr "" @@ -2023,9 +2062,15 @@ msgstr "" msgid "ClusterIntegration|Adding a Kubernetes cluster to your group will automatically share the cluster across all your projects. Use review apps, deploy your applications, and easily run your pipelines for all projects using the same cluster." msgstr "" +msgid "ClusterIntegration|Adding a Kubernetes cluster will automatically share the cluster across all projects. Use review apps, deploy your applications, and easily run your pipelines for all projects using the same cluster." +msgstr "" + msgid "ClusterIntegration|Adding an integration to your group will share the cluster across all your projects." msgstr "" +msgid "ClusterIntegration|Adding an integration will share the cluster across all projects." +msgstr "" + msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration" msgstr "" @@ -2206,6 +2251,9 @@ msgstr "" msgid "ClusterIntegration|Installing Knative may incur additional costs. Learn more about %{pricingLink}." msgstr "" +msgid "ClusterIntegration|Instance cluster" +msgstr "" + msgid "ClusterIntegration|Integrate Kubernetes cluster automation" msgstr "" @@ -2272,6 +2320,9 @@ msgstr "" msgid "ClusterIntegration|Learn more about group Kubernetes clusters" msgstr "" +msgid "ClusterIntegration|Learn more about instance Kubernetes clusters" +msgstr "" + msgid "ClusterIntegration|Let's Encrypt" msgstr "" @@ -2545,6 +2596,9 @@ msgstr "" msgid "Comment form position" msgstr "" +msgid "Comment is being updated" +msgstr "" + msgid "Comments" msgstr "" @@ -3072,6 +3126,9 @@ msgstr "" msgid "DashboardProjects|Personal" msgstr "" +msgid "DashboardProjects|Trending" +msgstr "" + msgid "Data is still calculating..." msgstr "" @@ -3383,6 +3440,9 @@ msgstr "" msgid "Disabled" msgstr "" +msgid "Disabled mirrors can only be enabled by instance owners. It is recommended that you delete them." +msgstr "" + msgid "Discard" msgstr "" @@ -3560,6 +3620,12 @@ msgstr "" msgid "EmailError|Your account has been blocked. If you believe this is in error, contact a staff member." msgstr "" +msgid "EmailToken|reset it" +msgstr "" + +msgid "EmailToken|resetting..." +msgstr "" + msgid "Emails" msgstr "" @@ -3866,6 +3932,9 @@ msgstr "" msgid "Error loading merge requests." msgstr "" +msgid "Error loading milestone tab" +msgstr "" + msgid "Error loading project data. Please try again." msgstr "" @@ -4094,6 +4163,18 @@ msgstr "" msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used." msgstr "" +msgid "ExternalMetrics|Add a button to the metrics dashboard linking directly to your existing external dashboards." +msgstr "" + +msgid "ExternalMetrics|Enter the URL of the dashboard you want to link to" +msgstr "" + +msgid "ExternalMetrics|External Dashboard" +msgstr "" + +msgid "ExternalMetrics|Full dashboard URL" +msgstr "" + msgid "ExternalWikiService|External Wiki" msgstr "" @@ -4142,6 +4223,9 @@ msgstr "" msgid "Failed to load errors from Sentry. Error message: %{errorMessage}" msgstr "" +msgid "Failed to load related branches" +msgstr "" + msgid "Failed to promote label due to internal error. Please contact administrators." msgstr "" @@ -4160,6 +4244,9 @@ msgstr "" msgid "Failed to remove user key." msgstr "" +msgid "Failed to save comment!" +msgstr "" + msgid "Failed to save merge conflicts resolutions. Please try again!" msgstr "" @@ -4172,6 +4259,9 @@ msgstr "" msgid "Failed to save preferences." msgstr "" +msgid "Failed to update branch!" +msgstr "" + msgid "Failed to update issues, please try again." msgstr "" @@ -4756,9 +4846,15 @@ msgstr "" msgid "Help page text and support page url." msgstr "" +msgid "Hide archived projects" +msgstr "" + msgid "Hide file browser" msgstr "" +msgid "Hide group projects" +msgstr "" + msgid "Hide host keys manual input" msgstr "" @@ -4768,6 +4864,9 @@ msgstr "" msgid "Hide payload" msgstr "" +msgid "Hide shared projects" +msgstr "" + msgid "Hide value" msgid_plural "Hide values" msgstr[0] "" @@ -4806,7 +4905,7 @@ msgstr "" msgid "I accept the|Terms of Service and Privacy Policy" msgstr "" -msgid "I have read and agree to the Let's Encrypt Terms of Service" +msgid "I have read and agree to the Let's Encrypt %{link_start}Terms of Service%{link_end}" msgstr "" msgid "ID" @@ -5151,6 +5250,9 @@ msgstr "" msgid "Issue events" msgstr "" +msgid "Issue update failed" +msgstr "" + msgid "IssueBoards|Board" msgstr "" @@ -5833,7 +5935,7 @@ msgstr "" msgid "MergeRequest|Error loading full diff. Please try again." msgstr "" -msgid "MergeRequest|Filter files" +msgid "MergeRequest|Filter files or search with %{modifier_key}+p" msgstr "" msgid "MergeRequest|No files found" @@ -5863,6 +5965,9 @@ msgstr "" msgid "Metrics for environment" msgstr "" +msgid "Metrics|Add metric" +msgstr "" + msgid "Metrics|Check out the CI/CD documentation on deploying to an environment" msgstr "" @@ -5953,6 +6058,9 @@ msgstr "" msgid "Mirroring settings were successfully updated." msgstr "" +msgid "Missing commit signatures endpoint!" +msgstr "" + msgid "MissingSSHKeyWarningLink|add an SSH key" msgstr "" @@ -6318,6 +6426,9 @@ msgstr "" msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token." msgstr "" +msgid "NoteForm|Note" +msgstr "" + msgid "Notes|Are you sure you want to cancel creating this comment?" msgstr "" @@ -6333,6 +6444,9 @@ msgstr "" msgid "Notes|Show history only" msgstr "" +msgid "Notes|This comment has changed since you started editing, please review the %{open_link}updated comment%{close_link} to ensure information is not lost" +msgstr "" + msgid "Nothing to preview." msgstr "" @@ -6527,6 +6641,12 @@ msgstr "" msgid "Overview" msgstr "" +msgid "Owned by anyone" +msgstr "" + +msgid "Owned by me" +msgstr "" + msgid "Owner" msgstr "" @@ -6917,12 +7037,33 @@ msgstr "" msgid "Preferences saved." msgstr "" +msgid "Preferences|Display time in 24-hour format" +msgstr "" + +msgid "Preferences|For example: 30 mins ago." +msgstr "" + msgid "Preferences|Navigation theme" msgstr "" +msgid "Preferences|These settings will update how dates and times are displayed for you." +msgstr "" + msgid "Preferences|This feature is experimental and translations are not complete yet" msgstr "" +msgid "Preferences|Time display" +msgstr "" + +msgid "Preferences|Time format" +msgstr "" + +msgid "Preferences|Time preferences" +msgstr "" + +msgid "Preferences|Use relative times" +msgstr "" + msgid "Press %{key}-C to copy" msgstr "" @@ -6965,6 +7106,9 @@ msgstr "" msgid "Private - The group and its projects can only be viewed by members." msgstr "" +msgid "Private profile" +msgstr "" + msgid "Private projects can be created in your personal namespace with:" msgstr "" @@ -7004,6 +7148,9 @@ msgstr "" msgid "Profiles|Avatar will be removed. Are you sure?" msgstr "" +msgid "Profiles|Bio" +msgstr "" + msgid "Profiles|Change username" msgstr "" @@ -7064,6 +7211,18 @@ msgstr "" msgid "Profiles|Enter your name, so people you know can recognize you" msgstr "" +msgid "Profiles|Feed token was successfully reset" +msgstr "" + +msgid "Profiles|Full name" +msgstr "" + +msgid "Profiles|Include private contributions on my profile" +msgstr "" + +msgid "Profiles|Incoming email token was successfully reset" +msgstr "" + msgid "Profiles|Increase your account's security by enabling Two-Factor Authentication (2FA)" msgstr "" @@ -7076,6 +7235,9 @@ msgstr "" msgid "Profiles|Learn more" msgstr "" +msgid "Profiles|Location" +msgstr "" + msgid "Profiles|Made a private contribution" msgstr "" @@ -7085,6 +7247,9 @@ msgstr "" msgid "Profiles|No file chosen" msgstr "" +msgid "Profiles|Organization" +msgstr "" + msgid "Profiles|Path" msgstr "" @@ -7094,6 +7259,9 @@ msgstr "" msgid "Profiles|Private contributions" msgstr "" +msgid "Profiles|Profile was successfully updated" +msgstr "" + msgid "Profiles|Public Avatar" msgstr "" @@ -7115,9 +7283,6 @@ msgstr "" msgid "Profiles|The maximum file size allowed is 200KB." msgstr "" -msgid "Profiles|There was an error with the reCAPTCHA. Please solve the reCAPTCHA again." -msgstr "" - msgid "Profiles|This doesn't look like a public SSH key, are you sure you want to add it?" msgstr "" @@ -7133,6 +7298,9 @@ msgstr "" msgid "Profiles|This information will appear on your profile" msgstr "" +msgid "Profiles|Time settings" +msgstr "" + msgid "Profiles|Two-Factor Authentication" msgstr "" @@ -7154,6 +7322,9 @@ msgstr "" msgid "Profiles|Use a private email - %{email}" msgstr "" +msgid "Profiles|User ID" +msgstr "" + msgid "Profiles|Username change failed - %{message}" msgstr "" @@ -7175,6 +7346,9 @@ msgstr "" msgid "Profiles|You can change your avatar here or remove the current avatar to revert to %{gravatar_link}" msgstr "" +msgid "Profiles|You can set your current timezone here" +msgstr "" + msgid "Profiles|You can upload your avatar here" msgstr "" @@ -7604,6 +7778,9 @@ msgstr "" msgid "Protected" msgstr "" +msgid "Protected Branch" +msgstr "" + msgid "Protected Tag" msgstr "" @@ -7891,6 +8068,9 @@ msgstr "" msgid "Reports|Class" msgstr "" +msgid "Reports|Classname" +msgstr "" + msgid "Reports|Execution time" msgstr "" @@ -8202,6 +8382,9 @@ msgstr "" msgid "Search files" msgstr "" +msgid "Search for a group" +msgstr "" + msgid "Search for projects, issues, etc." msgstr "" @@ -8229,6 +8412,9 @@ msgstr "" msgid "Search projects" msgstr "" +msgid "Search projects..." +msgstr "" + msgid "Search users" msgstr "" @@ -8526,6 +8712,12 @@ msgstr "" msgid "Show all activity" msgstr "" +msgid "Show archived projects" +msgstr "" + +msgid "Show archived projects only" +msgstr "" + msgid "Show command" msgstr "" @@ -8792,6 +8984,12 @@ msgstr "" msgid "SortOptions|Recent sign in" msgstr "" +msgid "SortOptions|Sort direction" +msgstr "" + +msgid "SortOptions|Stars" +msgstr "" + msgid "SortOptions|Start later" msgstr "" @@ -8954,6 +9152,9 @@ msgstr "" msgid "Storage:" msgstr "" +msgid "StorageSize|Unknown" +msgstr "" + msgid "Subgroups" msgstr "" @@ -9523,6 +9724,9 @@ msgstr "" msgid "This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention." msgstr "" +msgid "This also resolves the discussion" +msgstr "" + msgid "This application was created by %{link_to_owner}." msgstr "" @@ -10157,6 +10361,9 @@ msgstr "" msgid "Unable to schedule a pipeline to run immediately" msgstr "" +msgid "Unable to update this issue at this time." +msgstr "" + msgid "Unarchive project" msgstr "" @@ -10514,6 +10721,9 @@ msgstr "" msgid "View file @ " msgstr "" +msgid "View full dashboard" +msgstr "" + msgid "View group labels" msgstr "" @@ -10550,6 +10760,9 @@ msgstr "" msgid "Viewing commit" msgstr "" +msgid "Visibility" +msgstr "" + msgid "Visibility and access controls" msgstr "" @@ -10990,6 +11203,9 @@ msgstr "" msgid "You need permission." msgstr "" +msgid "You need to be logged in." +msgstr "" + msgid "You need to register a two-factor authentication app before you can set up a U2F device." msgstr "" @@ -11116,6 +11332,12 @@ msgstr "" msgid "Your changes have been successfully committed." msgstr "" +msgid "Your comment could not be submitted! Please check your network connection and try again." +msgstr "" + +msgid "Your comment could not be updated! Please check your network connection and try again." +msgstr "" + msgid "Your comment will not be visible to the public." msgstr "" diff --git a/package.json b/package.json index 77c7db59936..0cbc178a923 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,17 @@ { "private": true, "scripts": { + "check-dependencies": "yarn check --integrity", "clean": "rm -rf public/assets tmp/cache/*-loader", "dev-server": "NODE_OPTIONS=\"--max-old-space-size=3584\" nodemon -w 'config/webpack.config.js' --exec 'webpack-dev-server --config config/webpack.config.js'", "eslint": "eslint --max-warnings 0 --report-unused-disable-directives --ext .js,.vue .", "eslint-fix": "eslint --max-warnings 0 --report-unused-disable-directives --ext .js,.vue --fix .", "eslint-report": "eslint --max-warnings 0 --ext .js,.vue --format html --output-file ./eslint-report.html --no-inline-config .", + "prejest": "yarn check-dependencies", + "jest": "jest", "jest-debug": "node --inspect-brk node_modules/.bin/jest --runInBand", "jsdoc": "jsdoc -c config/jsdocs.config.js", + "prekarma": "yarn check-dependencies", "karma": "BABEL_ENV=${BABEL_ENV:=karma} karma start --single-run true config/karma.config.js", "karma-coverage": "BABEL_ENV=coverage karma start --single-run true config/karma.config.js", "karma-start": "BABEL_ENV=karma karma start config/karma.config.js", @@ -24,13 +28,13 @@ "webpack-prod": "NODE_OPTIONS=\"--max-old-space-size=3584\" NODE_ENV=production webpack --config config/webpack.config.js" }, "dependencies": { - "@babel/core": "^7.2.2", - "@babel/plugin-proposal-class-properties": "^7.3.0", + "@babel/core": "^7.4.4", + "@babel/plugin-proposal-class-properties": "^7.4.4", "@babel/plugin-proposal-json-strings": "^7.2.0", - "@babel/plugin-proposal-private-methods": "^7.3.0", + "@babel/plugin-proposal-private-methods": "^7.4.4", "@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/plugin-syntax-import-meta": "^7.2.0", - "@babel/preset-env": "^7.3.1", + "@babel/preset-env": "^7.4.4", "@gitlab/csslab": "^1.9.0", "@gitlab/svgs": "^1.60.0", "@gitlab/ui": "^3.10.0", @@ -179,7 +183,7 @@ "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^4.0.0-beta.0", "md5": "^2.2.1", - "node-sass": "^4.11.0", + "node-sass": "^4.12.0", "nodemon": "^1.18.9", "pixelmatch": "^4.0.2", "postcss": "^7.0.14", diff --git a/public/visual-review-toolbar.js b/public/visual-review-toolbar.js new file mode 100644 index 00000000000..dc21197bda9 --- /dev/null +++ b/public/visual-review-toolbar.js @@ -0,0 +1,626 @@ +/////////////////////////////////////////////// +/////////////////// STYLES //////////////////// +/////////////////////////////////////////////// + +const buttonClearStyles = ` + -webkit-appearance: none; +`; + +const buttonBaseStyles = ` + cursor: pointer; + transition: background-color 100ms linear, border-color 100ms linear, color 100ms linear, box-shadow 100ms linear; +`; + +const buttonSuccessActiveStyles = ` + background-color: #168f48; + border-color: #12753a; + color: #fff; +`; + +const buttonSuccessHoverStyles = ` + color: #fff; + background-color: #137e3f; + border-color: #127339; +`; + +const buttonSuccessStyles = ` + ${buttonBaseStyles} + background-color: #1aaa55; + border: 1px solid #168f48; + color: #fff; +`; + +const buttonSecondaryStyles = ` + ${buttonBaseStyles} + background: none #fff; + margin: 0 .5rem; + border: 1px solid #e3e3e3; +`; + +const buttonSecondaryActiveStyles = ` + color: #2e2e2e; + background-color: #e1e1e1; + border-color: #dadada; +`; + +const buttonSecondaryHoverStyles = ` + background-color: #f0f0f0; + border-color: #e3e3e3; + color: #2e2e2e; +`; + +const buttonWideStyles = ` + width: 100%; +`; + +const buttonWrapperStyles = ` + margin-top: 1rem; + display: flex; + align-items: baseline; + justify-content: flex-end; +`; + +const collapseStyles = ` + ${buttonBaseStyles} + width: 2.4rem; + height: 2.2rem; + margin-left: 1rem; + padding: .5rem; +`; + +const collapseClosedStyles = ` + ${collapseStyles} + align-self: center; +`; + +const collapseOpenStyles = ` + ${collapseStyles} +`; + +const checkboxLabelStyles = ` + padding: 0 .2rem; +`; + +const checkboxWrapperStyles = ` + display: flex; + align-items: baseline; +`; + +const formStyles = ` + display: flex; + flex-direction: column; + width: 100% +`; + +const labelStyles = ` + font-weight: 600; + display: inline-block; + width: 100%; +`; + +const linkStyles = ` + color: #1b69b6; + text-decoration: none; + background-color: transparent; + background-image: none; +`; + +const messageStyles = ` + padding: .25rem 0; + margin: 0; + line-height: 1.2rem; +`; + +const metadataNoteStyles = ` + font-size: .7rem; + line-height: 1rem; + color: #666; + margin-bottom: 0; +`; + +const inputStyles = ` + width: 100%; + border: 1px solid #dfdfdf; + border-radius: 4px; + padding: .1rem .2rem; + min-height: 2rem; + max-width: 17rem; +`; + +const svgInnerStyles = ` + pointer-events: none; +`; + +const wrapperClosedStyles = ` + max-width: 3.4rem; + max-height: 3.4rem; +`; + +const wrapperOpenStyles = ` + max-width: 22rem; + max-height: 22rem; +`; + +const wrapperStyles = ` + max-width: 22rem; + max-height: 22rem; + overflow: scroll; + position: fixed; + bottom: 1rem; + right: 1rem; + display: flex; + flex-direction: row-reverse; + padding: 1rem; + background-color: #fff; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, + 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', + 'Noto Color Emoji'; + font-size: .8rem; + font-weight: 400; + color: #2e2e2e; +`; + +const gitlabStyles = ` + #gitlab-collapse > * { + ${svgInnerStyles} + } + + #gitlab-form-wrapper { + ${formStyles} + } + + #gitlab-review-container { + ${wrapperStyles} + } + + .gitlab-open-wrapper { + ${wrapperOpenStyles} + } + + .gitlab-closed-wrapper { + ${wrapperClosedStyles} + } + + .gitlab-button-secondary { + ${buttonSecondaryStyles} + } + + .gitlab-button-secondary:hover { + ${buttonSecondaryHoverStyles} + } + + .gitlab-button-secondary:active { + ${buttonSecondaryActiveStyles} + } + + .gitlab-button-success:hover { + ${buttonSuccessHoverStyles} + } + + .gitlab-button-success:active { + ${buttonSuccessActiveStyles} + } + + .gitlab-button-success { + ${buttonSuccessStyles} + } + + .gitlab-button-wide { + ${buttonWideStyles} + } + + .gitlab-button-wrapper { + ${buttonWrapperStyles} + } + + .gitlab-collapse-closed { + ${collapseClosedStyles} + } + + .gitlab-collapse-open { + ${collapseOpenStyles} + } + + .gitlab-checkbox-label { + ${checkboxLabelStyles} + } + + .gitlab-checkbox-wrapper { + ${checkboxWrapperStyles} + } + + .gitlab-label { + ${labelStyles} + } + + .gitlab-link { + ${linkStyles} + } + + .gitlab-message { + ${messageStyles} + } + + .gitlab-metadata-note { + ${metadataNoteStyles} + } + + .gitlab-input { + ${inputStyles} + } +`; + +function addStylesheet() { + const styleEl = document.createElement('style'); + styleEl.insertAdjacentHTML('beforeend', gitlabStyles); + document.head.appendChild(styleEl); +} + +/////////////////////////////////////////////// +/////////////////// STATE //////////////////// +/////////////////////////////////////////////// +const data = {}; + +/////////////////////////////////////////////// +///////////////// COMPONENTS ////////////////// +/////////////////////////////////////////////// +const note = ` + <p id='gitlab-validation-note' class='gitlab-message'></p> +`; + +const comment = ` + <div> + <textarea id='gitlab-comment' name='gitlab-comment' rows='3' placeholder='Enter your feedback or idea' class='gitlab-input'></textarea> + ${note} + <p class='gitlab-metadata-note'>Additional metadata will be included: browser, OS, current page, user agent, and viewport dimensions.</p> + </div> + <div class='gitlab-button-wrapper''> + <button class='gitlab-button-secondary' style='${buttonClearStyles}' type='button' id='gitlab-logout-button'> Logout </button> + <button class='gitlab-button-success' style='${buttonClearStyles}' type='button' id='gitlab-comment-button'> Send feedback </button> + </div> +`; + +const commentIcon = ` + <svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>icn/comment</title><path d="M4 11.132l1.446-.964A1 1 0 0 1 6 10h5a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v6.132zM6.303 12l-2.748 1.832A1 1 0 0 1 2 13V5a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v4a3 3 0 0 1-3 3H6.303z" id="gitlab-comment-icon"/></svg> +` + +const compressIcon = ` + <svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>icn/compress</title><path d="M5.27 12.182l-1.562 1.561a1 1 0 0 1-1.414 0h-.001a1 1 0 0 1 0-1.415l1.56-1.56L2.44 9.353a.5.5 0 0 1 .353-.854H7.09a.5.5 0 0 1 .5.5v4.294a.5.5 0 0 1-.853.353l-1.467-1.465zm6.911-6.914l1.464 1.464a.5.5 0 0 1-.353.854H8.999a.5.5 0 0 1-.5-.5V2.793a.5.5 0 0 1 .854-.354l1.414 1.415 1.56-1.561a1 1 0 1 1 1.415 1.414l-1.561 1.56z" id="gitlab-compress-icon"/></svg> +`; + +const collapseButton = ` + <button id='gitlab-collapse' style='${buttonClearStyles}' class='gitlab-collapse-open gitlab-button-secondary'>${compressIcon}</button> +`; + + +const form = (content) => ` + <div id='gitlab-form-wrapper'> + ${content} + </div> +`; + +const login = ` + <div> + <label for='gitlab-token' class='gitlab-label'>Enter your <a class='gitlab-link' href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html">personal access token</a></label> + <input class='gitlab-input' type='password' id='gitlab-token' name='gitlab-token'> + ${note} + </div> + <div class='gitlab-checkbox-wrapper'> + <input type="checkbox" id="remember_token" name="remember_token" value="remember"> + <label for="remember_token" class='gitlab-checkbox-label'>Remember me</label> + </div> + <div class='gitlab-button-wrapper'> + <button class='gitlab-button-wide gitlab-button-success' style='${buttonClearStyles}' type='button' id='gitlab-login'> Submit </button> + </div> +`; + +/////////////////////////////////////////////// +//////////////// INTERACTIONS ///////////////// +/////////////////////////////////////////////// + +// from https://developer.mozilla.org/en-US/docs/Web/API/Window/navigator +function getBrowserId (sUsrAg) { + var aKeys = ["MSIE", "Edge", "Firefox", "Safari", "Chrome", "Opera"], + nIdx = aKeys.length - 1; + + for (nIdx; nIdx > -1 && sUsrAg.indexOf(aKeys[nIdx]) === -1; nIdx--); + return aKeys[nIdx]; +} + +function addCommentForm () { + const formWrapper = document.getElementById('gitlab-form-wrapper'); + formWrapper.innerHTML = comment; +} + +function addLoginForm () { + const formWrapper = document.getElementById('gitlab-form-wrapper'); + formWrapper.innerHTML = login; +} + +function authorizeUser () { + // Clear any old errors + clearNote('gitlab-token'); + + const token = document.getElementById('gitlab-token').value; + const rememberMe = document.getElementById('remember_token').checked; + + if (!token) { + postError('Please enter your token.', 'gitlab-token'); + return; + } + + if (rememberMe) { + storeToken(token); + } + + authSuccess(token); + return; +} + +function authSuccess (token) { + data.token = token; + addCommentForm(); +} + + +function clearNote (inputId) { + const note = document.getElementById('gitlab-validation-note'); + note.innerText = ''; + note.style.color = ''; + + if (inputId) { + const field = document.getElementById(inputId); + field.style.borderColor = ''; + } +} + +function confirmAndClear (discussionId) { + const commentButton = document.getElementById('gitlab-comment-button'); + const note = document.getElementById('gitlab-validation-note'); + + commentButton.innerText = 'Feedback sent'; + note.innerText = `Your comment was successfully posted to issue #${discussionId}`; + + setTimeout(resetCommentButton, 1000); +} + +function getInitialState () { + const { localStorage } = window; + + try { + let token = localStorage.getItem('token'); + + if (token) { + data.token = token; + return comment; + } + + return login; + + } catch (err) { + return login; + } +} + +function getProjectDetails () { + const { innerWidth, + innerHeight, + location: { href }, + navigator: { + platform, userAgent + } } = window; + const browser = getBrowserId(userAgent); + + const scriptEl = document.getElementById('review-app-toolbar-script') + const { projectId, discussionId, mrUrl } = scriptEl.dataset; + + return { + href, + platform, + browser, + userAgent, + innerWidth, + innerHeight, + projectId, + discussionId, + mrUrl, + }; +} + +function logoutUser () { + const { localStorage } = window; + + // All the browsers we support have localStorage, so let's silently fail + // and go on with the rest of the functionality. + try { + localStorage.removeItem('token'); + } catch (err) { + return; + } + + addLoginForm(); +} + +function postComment ({ + href, + platform, + browser, + userAgent, + innerWidth, + innerHeight, + projectId, + discussionId, + mrUrl, +}) { + // Clear any old errors + clearNote('gitlab-comment'); + + setInProgressState(); + + const commentText = document.getElementById('gitlab-comment').value.trim(); + + if (!commentText) { + postError('Your comment appears to be empty.', 'gitlab-comment'); + resetCommentBox(); + return; + } + + const detailText = ` + <details> + <summary>Metadata</summary> + Posted from ${href} | ${platform} | ${browser} | ${innerWidth} x ${innerHeight}. + <br /><br /> + *User agent: ${userAgent}* + </details> + `; + + const url = ` + ${mrUrl}/api/v4/projects/${projectId}/issues/${discussionId}/discussions`; + + const body = `${commentText}${detailText}`; + + fetch(url, { + method: 'POST', + headers: { + 'PRIVATE-TOKEN': data.token, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ body }), + }) + .then((response) => { + if (response.ok) { + confirmAndClear(discussionId); + return; + } + + throw new Error(`${response.status}: ${response.statusText}`) + }) + .catch((err) => { + postError(`The feedback was not sent successfully. Please try again. Error: ${err.message}`, 'gitlab-comment'); + resetCommentBox(); + }); +} + +function postError (message, inputId) { + const note = document.getElementById('gitlab-validation-note'); + const field = document.getElementById(inputId); + field.style.borderColor = '#db3b21'; + note.style.color = '#db3b21'; + note.innerText = message; +} + +function resetCommentBox() { + const commentBox = document.getElementById('gitlab-comment'); + const commentButton = document.getElementById('gitlab-comment-button'); + + commentButton.innerText = 'Send feedback'; + commentButton.classList.replace('gitlab-button-secondary', 'gitlab-button-success'); + commentButton.style.opacity = 1; + + commentBox.style.pointerEvents = 'auto'; + commentBox.style.color = 'rgba(0, 0, 0, 1)'; +} + +function resetCommentButton() { + const commentBox = document.getElementById('gitlab-comment'); + const note = document.getElementById('gitlab-validation-note'); + + commentBox.value = ''; + note.innerText = ''; + resetCommentBox(); +} + +function setInProgressState() { + const commentButton = document.getElementById('gitlab-comment-button'); + const commentBox = document.getElementById('gitlab-comment'); + + commentButton.innerText = 'Sending feedback'; + commentButton.classList.replace('gitlab-button-success', 'gitlab-button-secondary'); + commentButton.style.opacity = 0.5; + commentBox.style.color = 'rgba(223, 223, 223, 0.5)'; + commentBox.style.pointerEvents = 'none'; +} + +function storeToken (token) { + + const { localStorage } = window; + + // All the browsers we support have localStorage, so let's silently fail + // and go on with the rest of the functionality. + try { + localStorage.setItem('token', token); + } catch (err) { + return; + } +} + +function toggleForm () { + const container = document.getElementById('gitlab-review-container'); + const collapseButton = document.getElementById('gitlab-collapse'); + const form = document.getElementById('gitlab-form-wrapper'); + const OPEN = 'open'; + const CLOSED = 'closed'; + + const stateVals = { + [OPEN]: { + buttonClasses: ['gitlab-collapse-closed', 'gitlab-collapse-open'], + containerClasses: ['gitlab-closed-wrapper', 'gitlab-open-wrapper'], + icon: compressIcon, + display: 'flex', + backgroundColor: 'rgba(255, 255, 255, 1)', + }, + [CLOSED]: { + buttonClasses: ['gitlab-collapse-open', 'gitlab-collapse-closed'], + containerClasses: ['gitlab-open-wrapper', 'gitlab-closed-wrapper'], + icon: commentIcon, + display: 'none', + backgroundColor: 'rgba(255, 255, 255, 0)', + }, + } + + const nextState = collapseButton.classList.contains('gitlab-collapse-open') ? CLOSED : OPEN; + + container.classList.replace(...stateVals[nextState].containerClasses); + container.style.backgroundColor = stateVals[nextState].backgroundColor; + form.style.display = stateVals[nextState].display; + collapseButton.classList.replace(...stateVals[nextState].buttonClasses); + collapseButton.innerHTML = stateVals[nextState].icon; +} + +/////////////////////////////////////////////// +///////////////// INJECTION ////////////////// +/////////////////////////////////////////////// + +function noop() {}; + +const eventLookup = ({target: { id }}) => { + switch (id) { + case 'gitlab-collapse': + return toggleForm; + case 'gitlab-comment-button': + const projectDetails = getProjectDetails(); + return postComment.bind(null, projectDetails); + case 'gitlab-login': + return authorizeUser; + case 'gitlab-logout-button': + return logoutUser; + default: + return noop; + } +}; + +window.addEventListener('load', () => { + const content = getInitialState(); + const container = document.createElement('div'); + + container.setAttribute('id', 'gitlab-review-container'); + container.insertAdjacentHTML('beforeend', collapseButton); + container.insertAdjacentHTML('beforeend', form(content)); + + document.body.insertBefore(container, document.body.firstChild); + addStylesheet(); + + document.getElementById('gitlab-review-container').addEventListener('click', (event) => { + eventLookup(event)(); + }); +}); diff --git a/qa/Dockerfile b/qa/Dockerfile index ca7f9accb70..74be373b8e8 100644 --- a/qa/Dockerfile +++ b/qa/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:2.5-stretch +FROM ruby:2.6-stretch LABEL maintainer "Grzegorz Bizon <grzegorz@gitlab.com>" ENV DEBIAN_FRONTEND noninteractive diff --git a/qa/README.md b/qa/README.md index 8efdd8514f1..002ad4c65f5 100644 --- a/qa/README.md +++ b/qa/README.md @@ -49,8 +49,10 @@ will need to [modify your GDK setup](https://gitlab.com/gitlab-org/gitlab-qa/blo ### Writing tests -1. [Using page objects](qa/page/README.md) -2. [Style guide](STYLE_GUIDE.md) +- [Writing tests from scratch tutorial](docs/WRITING_TESTS_FROM_SCRATCH.md) + - [Best practices](docs/BEST_PRACTICES.md) + - [Using page objects](qa/page/README.md) + - [Guidelines](docs/GUIDELINES.md) ### Running specific tests diff --git a/qa/docs/BEST_PRACTICES.md b/qa/docs/BEST_PRACTICES.md new file mode 100644 index 00000000000..3a2640607e4 --- /dev/null +++ b/qa/docs/BEST_PRACTICES.md @@ -0,0 +1,38 @@ +# Best practices when writing end-to-end tests + +The majority of the end-to-end tests require some state to be built in the application for the tests to happen. + +A good example is a user being logged in as a pre-condition for testing the feature. + +But if the login feature is already covered with end-to-end tests through the GUI, there is no reason to perform such an expensive task to test the functionality of creating a project, or importing a repo, even if this features depend on a user being logged in. Let's see an example to make things clear. + +Let's say that, on average, the process to perform a successful login through the GUI takes 2 seconds. + +Now, realize that almost all tests need the user to be logged in, and that we need every test to run in isolation, meaning that tests cannot interfere with each other. This would mean that for every test the user needs to log in, and "waste 2 seconds". + +Now, multiply the number of tests per 2 seconds, and as your test suite grows, the time to run it grows with it, and this is not sustainable. + +An alternative to perform a login in a cheaper way would be having an endpoint (available only for testing) where we could pass the user's credentials as encrypted values as query strings, and then we would be redirected to the logged in home page if the credentials are valid. Let's say that, on average, this process takes only 200 miliseconds. + +You see the point right? + +Performing a login through the GUI for every test would cost a lot in terms of tests' execution. + +And there is another reason. + +Let's say that you don't follow the above suggestion, and depend on the GUI for the creation of every application state in order to test a specific feature. In this case we could be talking about the **Issues** feature, that depends on a project to exist, and the user to be logged in. + +What would happen if there was a bug in the project creation page, where the 'Create' button is disabled, not allowing for the creation of a project through the GUI, but the API logic is still working? + +In this case, instead of having only the project creation test failing, we would have many tests that depend on a project to be failing too. + +But, if we were following the best practices, only one test would be failing, and tests for other features that depend on a project to exist would continue to pass, since they could be creating the project behind the scenes interacting directly with the public APIs, ensuring a more reliable metric of test failure rate. + +Finally, interacting with the application only by its GUI generates a higher rate of test flakiness, and we want to avoid that at max. + +**The takeaways here are:** + +- Building state through the GUI is time consuming and it's not sustainable as the test suite grows. +- When depending only on the GUI to create the application's state and tests fail due to front-end issues, we can't rely on the test failures rate, and we generates a higher rate of test flakiness. + +Now that we are aware of all of it, [let's go create some tests](./WRITING_TESTS_FROM_SCRATCH.md). diff --git a/qa/STYLE_GUIDE.md b/qa/docs/GUIDELINES.md index 900f7456e1a..9db52cd07e6 100644 --- a/qa/STYLE_GUIDE.md +++ b/qa/docs/GUIDELINES.md @@ -43,4 +43,4 @@ end Notice that in the above example, before clicking the `:operations_environments_link`, another element is hovered over. -> We can create these methods as helpers to abstract multi-step navigation.
\ No newline at end of file +> We can create these methods as helpers to abstract multi-step navigation. diff --git a/qa/docs/WRITING_TESTS_FROM_SCRATCH.md b/qa/docs/WRITING_TESTS_FROM_SCRATCH.md new file mode 100644 index 00000000000..a6daffc964e --- /dev/null +++ b/qa/docs/WRITING_TESTS_FROM_SCRATCH.md @@ -0,0 +1,380 @@ +# Writing end-to-end tests step-by-step + +In this tutorial, you will find different examples, and the steps involved, in the creation of end-to-end (_e2e_) tests for GitLab CE and GitLab EE, using GitLab QA. + +> When referring to end-to-end tests in this document, this means testing a specific feature end-to-end, such as a user logging in, the creation of a project, the management of labels, breaking down epics into sub-epics and issues, etc. + +## Important information before we start writing tests + +It's important to understand that end-to-end tests of isolated features, such as the ones described in the above note, doesn't mean that everything needs to happen through the GUI. + +If you don't exactly understand what we mean by **not everything needs to happen through the GUI,** please make sure you've read the [best practices](./BEST_PRACTICES.md) before moving on. + +## This document covers the following items: + +0. Identifying if end-to-end tests are really needed +1. Identifying the [DevOps stage](https://about.gitlab.com/stages-devops-lifecycle/) of the feature that you are going to cover with end-to-end tests +2. Creating the skeleton of the test file (`*_spec.rb`) +3. The [MVC](https://about.gitlab.com/handbook/values/#minimum-viable-change-mvc) of the test cases logic +4. Extracting duplicated code into methods +5. Tests' pre-conditions (`before :all` and `before`) using resources and [Page Objects](./qa/page/README.md) +6. Optimizing the test suite +7. Using and implementing resources +8. Moving elements definitions and its methods to [Page Objects](./qa/page/README.md) + - Adding testability to the application + +### 0. Are end-to-end tests needed? + +At GitLab we respect the [test pyramid](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/testing_guide/testing_levels.md), and so, we recommend to check the code coverage of a specific feature before writing end-to-end tests. + +Sometimes you may notice that there is already a good coverage in other test levels, and we can stay confident that if we break a feature, we will still have quick feedback about it, even without having end-to-end tests. + +If after this analysis you still think that end-to-end tests are needed, keep reading. + +### 1. Identifying the DevOps stage + +The GitLab QA end-to-end tests are organized by the different [stages in the DevOps lifecycle](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/qa/specs/features/browser_ui), and so, if you are creating tests for issue creation, for instance, you would locate the spec files under the `qa/qa/specs/features/browser_ui/2_plan/` directory since issue creation is part of the Plan stage. + + In another case of a test for listing merged merge requests (MRs), the test should go under the `qa/qa/specs/features/browser_ui/3_create/` directory since merge request is a feature from the Create stage. + +> There may be sub-directories inside the stages directories, for different features. For example: `.../browser_ui/2_plan/ee_epics/` and `.../browser_ui/2_plan/issues/`. + +Now, let's say we want to create tests for the [scoped labels](https://about.gitlab.com/2019/04/22/gitlab-11-10-released/#scoped-labels) feature, available on GitLab EE Premium (this feature is part of the Plan stage.) + +> Because these tests are for a feature available only on GitLab EE, we need to create them in the [EE repository](https://gitlab.com/gitlab-org/gitlab-ee). + +Since [there is no specific directory for this feature](https://gitlab.com/gitlab-org/gitlab-ee/tree/master/qa/qa/specs/features/browser_ui/2_plan), we should create a sub-directory for it. + +Under `.../browser_ui/2_plan/`, let's create a sub-directory called `ee_scoped_labels/`. + +> Notice that since this feature is only available for GitLab EE we prefix the sub-directory with `ee_`. + +### 2. Test skeleton + +Inside the newly created sub-directory, let's create a file describing the test suite (e.g. `editing_scoped_labels_spec.rb`.) + +#### The `context` and `describe` blocks + +Specs have an outer `context` that indicates the DevOps stage. The next level is the `describe` block, that briefly states the subject of the test suite. See the following example: + +```ruby +module QA + context 'Plan' do + describe 'Editing scoped labels properties on issues' do + end + end +end +``` + +#### The `it` blocks + +Every test suite is composed by at least one `it` block, and a good way to start writing end-to-end tests is by typing test cases descriptions as `it` blocks. Take a look at the following example: + +```ruby +module QA + context 'Plan' do + describe 'Editing scoped labels properties on issues' do + it 'replaces an existing label if it has the same key' do + end + + it 'keeps both scoped labels when adding a label with a different key' do + end + end + end +end +``` + +### 3. Test cases MVC + +For the [MVC](https://about.gitlab.com/handbook/values/#minimum-viable-change-mvc) of our test cases, let's say that we already have the application in the state needed for the tests, and then let's focus on the logic of the test cases only. + +To evolve the test cases drafted on step 2, let's imagine that the user is already logged in a GitLab EE instance, they already have at least a Premium license in use, there is already a project created, there is already an issue opened in the project, the issue already has a scoped label (e.g. `foo::bar`), there are other scoped labels (for the same scope and for a different scope, e.g. `foo::baz` and `bar::bah`), and finally, the user is already on the issue's page. Let's also suppose that for every test case the application is in a clean state, meaning that one test case won't affect another. + +> Note: there are different approaches to create an application state for end-to-end tests. Some of them are very time consuming and subject to failures, such as when using the GUI for all the pre-conditions of the tests. On the other hand, other approaches are more efficient, such as using the public APIs. The latter is more efficient since it doesn't depend on the GUI. We won't focus on this part yet, but it's good to keep it in mind. + +Let's now focus on the first test case. + +```ruby +it 'keeps the latest scoped label when adding a label with the same key of an existing one, but with a different value' do + # This implementation is only for tutorial purposes. We normally encapsulate elements in Page Objects. + page.find('.block.labels .edit-link').click + page.find('.dropdown-menu-labels .dropdown-input-field').send_keys ['foo::baz', :enter] + page.find('#content-body').click + page.refresh + + scoped_label = page.find('.qa-labels-block .scoped-label-wrapper') + + expect(scoped_label).to have_content('foo::baz') + expect(scoped_label).not_to have_content('foo::bar') + expect(page).to have_content('added foo::baz label and removed foo::bar') +end +``` + +> Notice that the test itself is simple. The most challenging part is the creation of the application state, which will be covered later. + +> The exemplified test cases' MVC is not enough for the change to be submitted in an MR, but they help on building up the test logic. The reason is that we do not want to use locators directly in the tests, and tests **must** use [Page Objects](./qa/page/README.md) before they can be merged. + +Below are the steps that the test covers: + +1. The test finds the 'Edit' link for the labels and clicks on it +2. Then it fills in the 'Assign labels' input field with the value 'foo::baz' and press enter +3. Then it clicks in the content body to apply the label and refreshes the page +4. Finally the expectation that the previous scoped label was removed and that the new one was added happens + +Let's now see how the second test case would look like. + +```ruby +it 'keeps both scoped labels when adding a label with a different key' do + # This implementation is only for tutorial purposes. We normally encapsulate elements in Page Objects. + page.find('.block.labels .edit-link').click + page.find('.dropdown-menu-labels .dropdown-input-field').send_keys ['bar::bah', :enter] + page.find('#content-body').click + page.refresh + + scoped_labels = page.all('.qa-labels-block .scoped-label-wrapper') + + expect(scoped_labels.first).to have_content('bar::bah') + expect(scoped_labels.last).to have_content('foo::ba') + expect(page).to have_content('added bar::bah') + expect(page).to have_content('added foo::ba') +end +``` + +> Note that elements are always located using CSS selectors, and a good practice is to add test specific attribute:value for elements (this is called adding testability to the application and we will talk more about it later.) + +Below are the steps that the test covers: + +1. The test finds the 'Edit' link for the labels and clicks on it +2. Then it fills in the 'Assign labels' input field with the value 'bar::bah' and press enter +3. Then it clicks in the content body to apply the label and refreshes the page +4. Finally the expectation that the both scoped labels are present happens + +> Similar to the previous test, this one is also very straight forward, but there is some code duplication. Let's address it. + +### 4. Extracting duplicated code + +If we refactor the tests created on step 3 we could come up with something like this: + +```ruby +it 'keeps the latest scoped label when adding a label with the same key of an existing one, but with a different value' do + select_label_and_refresh 'foo::baz' + + expect(page).to have_content('added foo::baz') + expect(page).to have_content('and removed foo::bar') + + scoped_label = page.find('.qa-labels-block .scoped-label-wrapper') + + expect(scoped_label).to have_content('foo::baz') + expect(scoped_label).not_to have_content('foo::bar') +end + +it 'keeps both scoped label when adding a label with a different key' do + select_label_and_refresh 'bar::bah' + + expect(page).to have_content('added bar::bah') + expect(page).to have_content('added foo::ba') + + scoped_labels = page.all('.qa-labels-block .scoped-label-wrapper') + + expect(scoped_labels.first).to have_content('bar::bah') + expect(scoped_labels.last).to have_content('foo::ba') +end + +def select_label_and_refresh(label) + page.find('.block.labels .edit-link').click + page.find('.dropdown-menu-labels .dropdown-input-field').send_keys [label, :enter] + page.find('#content-body').click + page.refresh +end +``` + +By creating a reusable `select_label_and_refresh` method we remove the code duplication, and later we can move this method to a Page Object class that will be created for easier maintenance purposes. + +> Notice that the reusable method is created in the bottom of the file. The reason for that is that reading the code should be similar to reading a newspaper, where high-level information is at the top, like the title and summary of the news, while low level, or more specific information, is at the bottom. + +### 5. Tests' pre-conditions using resources and Page Objects + +In this section, we will address the previously mentioned subject of creating the application state for the tests, using the `before :all` and `before` blocks, together with resources and Page Objects. + +#### `before :all` + +A pre-condition for the entire test suite is defined in the `before :all` block. + +For our test suite example, some things that could happen before the entire test suite starts are: + +- The user logging in; +- A premium license already being set up; +- A project being created with an issue and labels already setup. + +> In case of a test suite with only one `it` block it's ok to use only the `before` block (see below) with all the test's pre-conditions. + +#### `before` + +A pre-condition for each test case is defined in the `before` block. + +For our test cases samples, what we need is that for every test the issue page is opened, and there is only one scoped label applied to it. + +#### Implementation + +In the following code we will focus on the test suite and the test cases' pre-conditions only: + +```ruby +module QA + context 'Plan' do + describe 'Editing scoped labels properties on issues' do + before :all do + project = Resource::Project.fabricate_via_api! do |resource| + resource.name = 'scoped-labels-project' + end + + @foo_bar_scoped_label = 'foo::bar' + + @issue = Resource::Issue.fabricate_via_api! do |issue| + issue.project = project + issue.title = 'Issue to test the scoped labels' + issue.labels = @foo_bar_scoped_label + end + + @labels = ['foo::baz', 'bar::bah'] + @labels.each do |label| + Resource::Label.fabricate_via_api! do |l| + l.project = project.id + l.title = label + end + end + + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.perform(&:sign_in_using_credentials) + end + + before do + Page::Project::Issue::Show.perform do |issue_page| + @issue.visit! + end + end + + it 'keeps the latest scoped label when adding a label with the same key of an existing one, but with a different value' do + ... + end + + it 'keeps both scoped labels when adding a label with a different key' do + ... + end + + def select_label_and_refresh(label) + ... + end + end + end +end +``` + +In the `before :all` block we create all the application state needed for the tests to run. We do that by fabricating resources via APIs (`project`, `@issue`, and `@labels`), by using the `Runtime::Browser.visit` method to go to the login page, and by performing a `sign_in_using_credentials` from the `Login` Page Object. + +> When creating the resources, notice that when calling the `fabricate_via_api` method, we pass some attribute:values, like `name` for the `project` resoruce, `project`, `title`, and `labels` for the the issue resource, and `project`, and `title` for `label` resources. + +> What's important to understand here is that by creating the application state mostly using the public APIs we save a lot of time in the test suite setup stage. + +> Soon we will cover the use of the already existing resources' methods and the creation of your own `fabricate_via_api` methods for resources where this is still not available, but first, let's optimize our implementation. + +### 6. Optimization + +As already mentioned in the [best practices](./BEST_PRACTICES.md) document, end-to-end tests are very costly in terms of execution time, and it's our responsibility as software engineers to ensure that we optimize them as max as possible. + +> Differently than unit tests, that exercise every little piece of the application in isolation, usually having only one assertion per test, and being very fast to run, end-to-end tests can have more actions and assertions in a single test to help on speeding up the test's feedback since they are much slower when comparing to unit tests. + +Some improvements that we could make in our test suite to optimize its time to run are: + +1. Having a single test case (an `it` block) that exercise both scenarios to avoid "wasting" time in the tests' pre-conditions, instead of having two different test cases. +2. Moving all the pre-conditions to the `before` block since there will be only one `it` block. +3. Making the selection of labels more performant by allowing for the selection of more than one label in the same reusable method. + +Let's look at a suggestion that addresses the above points, one by one: + +```ruby +module QA + context 'Plan' do + describe 'Editing scoped labels properties on issues' do + before do + project = Resource::Project.fabricate_via_api! do |resource| + resource.name = 'scoped-labels-project' + end + + @foo_bar_scoped_label = 'foo::bar' + + @issue = Resource::Issue.fabricate_via_api! do |issue| + issue.project = project + issue.title = 'Issue to test the scoped labels' + issue.labels = @foo_bar_scoped_label + end + + @labels = ['foo::baz', 'bar::bah'] + @labels.each do |label| + Resource::Label.fabricate_via_api! do |l| + l.project = project.id + l.title = label + end + end + + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.perform(&:sign_in_using_credentials) + Page::Project::Issue::Show.perform do |issue_page| + @issue.visit! + end + end + + it 'correctly applies the scoped labels depending if they are from the same or a different scope' do + select_labels_and_refresh @labels + + scoped_labels = page.all('.qa-labels-block .scoped-label-wrapper') + + expect(page).to have_content("added #{@foo_bar_scoped_label}") + expect(page).to have_content("added #{@labels[1]} #{@labels[0]} labels and removed #{@foo_bar_scoped_label}") + expect(scoped_labels.count).to eq(2) + expect(scoped_labels.first).to have_content(@labels[1]) + expect(scoped_labels.last).to have_content(@labels[0]) + end + + def select_labels_and_refresh(labels) + find('.block.labels .edit-link').click + labels.each do |label| + find('.dropdown-menu-labels .dropdown-input-field').send_keys [label, :enter] + end + find('#content-body').click + refresh + end + end + end + end +``` + +As you can see, now all the pre-conditions from the `before :all` block were moved to the `before` block, addressing point 2. + +To address point 1, we changed the test implementation from two `it` blocks into a single one that exercises both scenarios. Now the new test description is: `'correctly applies the scoped labels depending if they are from the same or a different scope'`. It's a long description, but it describes well what the test does. + +> Notice that the implementation of the new and unique `it` block had to change a little bit. Below we describe in details what it does. + +1. At the same time, it selects two scoped labels, one from the same scope of the one already applied in the issue during the setup phase (in the `before` block), and another one from a different scope. +2. It runs the assertions that the labels where correctly added and removed; that only two labels are applied; and that those are the correct ones, and that they are shown in the right order. + +Finally, the `select_label_and_refresh` method is changed to `select_labels_and_refresh`, which accepts an array of labels instead of a single label, and it iterates on them for faster label selection (this is what is used in step 1 explained above.) + +### 7. Resources + +TBD. + +### 8. Page Objects + +> Page Objects are auto-loaded in the `qa/qa.rb` file and available in all the test files (`*_spec.rb`). + +Page Objects are used in end-to-end tests for maintenance reasons, where page's elements and methods are defined to be reused in any test. + +Take a look at [this document that specifically details the usage of Page Objects](./qa/page/README.md). + +Now, let's go back to our examples. + +... + +#### Adding testability + +TBD.
\ No newline at end of file diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index 9fabf83e2ce..c395e5f6011 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -113,8 +113,8 @@ module QA has_css?(element_selector_css(name), wait: wait, text: text) end - def has_no_element?(name, wait: Capybara.default_max_wait_time) - has_no_css?(element_selector_css(name), wait: wait) + def has_no_element?(name, text: nil, wait: Capybara.default_max_wait_time) + has_no_css?(element_selector_css(name), wait: wait, text: text) end def has_text?(text) @@ -129,8 +129,8 @@ module QA has_no_css?('.fa-spinner', wait: Capybara.default_max_wait_time) end - def within_element(name) - page.within(element_selector_css(name)) do + def within_element(name, text: nil) + page.within(element_selector_css(name), text: text) do yield end end diff --git a/qa/qa/page/project/branches/show.rb b/qa/qa/page/project/branches/show.rb index 922a6ddb086..762eacdab15 100644 --- a/qa/qa/page/project/branches/show.rb +++ b/qa/qa/page/project/branches/show.rb @@ -7,6 +7,7 @@ module QA class Show < Page::Base view 'app/views/projects/branches/_branch.html.haml' do element :remove_btn + element :branch_name end view 'app/views/projects/branches/_panel.html.haml' do element :all_branches @@ -27,11 +28,9 @@ module QA finished_loading? end - def has_branch_title?(branch_title) + def has_no_branch?(branch_name) within_element(:all_branches) do - within(".item-title") do - has_text?(branch_title) - end + has_no_element?(:branch_name, text: branch_name, wait: Support::Waiter::DEFAULT_MAX_WAIT_TIME) end end @@ -48,15 +47,6 @@ module QA click_element(:delete_merged_branches) end end - - def wait_for_texts_not_to_be_visible(texts) - text_not_visible = wait do - texts.all? do |text| - has_no_text?(text) - end - end - raise "Expected text(s) #{texts} not to be visible" unless text_not_visible - end end end end diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb index a118176eb8a..15cd59f041b 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true module QA - context 'Manage', :orchestrated, :oauth do + # https://gitlab.com/gitlab-org/quality/nightly/issues/100 + context 'Manage', :orchestrated, :oauth, :quarantine do describe 'OAuth login' do it 'User logs in to GitLab with GitHub OAuth' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb index ac34f72bb8f..c0d597af076 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb @@ -5,7 +5,7 @@ module QA describe 'filter issue comments activities' do let(:issue_title) { 'issue title' } - it 'user filters comments and activites in an issue' do + it 'user filters comments and activities in an issue' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb index 9e48ee7ca2a..db33c6330ff 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true module QA - context 'Create' do + # https://gitlab.com/gitlab-org/quality/staging/issues/55 + context 'Create', :quarantine do describe 'Download merge request patch and diff' do before(:context) do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb index c2c2b6da90a..cf6c24fa873 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb @@ -73,10 +73,9 @@ module QA Page::Project::Branches::Show.perform do |branches_view| branches_view.delete_branch(third_branch) + expect(branches_view).to have_no_branch(third_branch) end - expect(page).not_to have_content(third_branch) - Page::Project::Branches::Show.perform(&:delete_merged_branches) expect(page).to have_content( @@ -85,8 +84,7 @@ module QA page.refresh Page::Project::Branches::Show.perform do |branches_view| - branches_view.wait_for_texts_not_to_be_visible([commit_message_of_second_branch]) - expect(branches_view).not_to have_branch_title(second_branch) + expect(branches_view).to have_no_branch(second_branch) end end end diff --git a/qa/qa/support/page/logging.rb b/qa/qa/support/page/logging.rb index 69b6332ecce..ff505fdbddd 100644 --- a/qa/qa/support/page/logging.rb +++ b/qa/qa/support/page/logging.rb @@ -76,23 +76,18 @@ module QA super end - def has_element?(name, text: nil, wait: Capybara.default_max_wait_time) + def has_element?(name, **kwargs) found = super - msg = ["has_element? :#{name}"] - msg << %Q(with text "#{text}") if text - msg << "(wait: #{wait})" - msg << "returned: #{found}" - - log(msg.compact.join(' ')) + log_has_element_or_not('has_element?', name, found, **kwargs) found end - def has_no_element?(name, wait: Capybara.default_max_wait_time) + def has_no_element?(name, **kwargs) found = super - log("has_no_element? :#{name} returned #{found}") + log_has_element_or_not('has_no_element?', name, found, **kwargs) found end @@ -149,6 +144,15 @@ module QA def log(msg) QA::Runtime::Logger.debug(msg) end + + def log_has_element_or_not(method, name, found, **kwargs) + msg = ["#{method} :#{name}"] + msg << %Q(with text "#{kwargs[:text]}") if kwargs[:text] + msg << "(wait: #{kwargs[:wait] || Capybara.default_max_wait_time})" + msg << "returned: #{found}" + + log(msg.compact.join(' ')) + end end end end diff --git a/qa/qa/support/waiter.rb b/qa/qa/support/waiter.rb index 21a399b4a5f..fdcf2d7e157 100644 --- a/qa/qa/support/waiter.rb +++ b/qa/qa/support/waiter.rb @@ -3,9 +3,11 @@ module QA module Support module Waiter + DEFAULT_MAX_WAIT_TIME = 60 + module_function - def wait(max: 60, interval: 0.1) + def wait(max: DEFAULT_MAX_WAIT_TIME, interval: 0.1) QA::Runtime::Logger.debug("with wait: max #{max}; interval #{interval}") start = Time.now diff --git a/qa/spec/page/logging_spec.rb b/qa/spec/page/logging_spec.rb index 707a7ff6d98..99e96b81a51 100644 --- a/qa/spec/page/logging_spec.rb +++ b/qa/spec/page/logging_spec.rb @@ -93,7 +93,14 @@ describe QA::Support::Page::Logging do allow(page).to receive(:has_no_css?).and_return(true) expect { subject.has_no_element?(:element) } - .to output(/has_no_element\? :element returned true/).to_stdout_from_any_process + .to output(/has_no_element\? :element \(wait: 2\) returned: true/).to_stdout_from_any_process + end + + it 'logs has_no_element? with text' do + allow(page).to receive(:has_no_css?).and_return(true) + + expect { subject.has_no_element?(:element, text: "more text") } + .to output(/has_no_element\? :element with text \"more text\" \(wait: 2\) returned: true/).to_stdout_from_any_process end it 'logs has_text?' do diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb index 24ff1523ba7..a368ffba711 100644 --- a/qa/spec/spec_helper.rb +++ b/qa/spec/spec_helper.rb @@ -35,8 +35,10 @@ RSpec.configure do |config| # show exception that triggers a retry if verbose_retry is set to true config.display_try_failure_messages = true - config.around do |example| - retry_times = example.metadata.keys.include?(:quarantine) ? 1 : 2 - example.run_with_retry retry: retry_times + if ENV['CI'] + config.around do |example| + retry_times = example.metadata.keys.include?(:quarantine) ? 1 : 2 + example.run_with_retry retry: retry_times + end end end diff --git a/scripts/clean-old-cached-assets b/scripts/clean-old-cached-assets index 7a3a62a477a..8bdd3a9cdb6 100755 --- a/scripts/clean-old-cached-assets +++ b/scripts/clean-old-cached-assets @@ -1,6 +1,6 @@ #!/bin/bash -# Clean up cached files that are older than 1 week -find tmp/cache/assets/sprockets/ -type f -mtime +7 -execdir rm -- "{}" \; +# Clean up cached files that are older than 4 days +find tmp/cache/assets/sprockets/ -type f -mtime +4 -execdir rm -- "{}" \; du -d 0 -h tmp/cache/assets/sprockets | cut -f1 | xargs -I % echo "tmp/cache/assets/sprockets/ is currently %" diff --git a/scripts/frontend/stylelint/stylelint-utility-map.js b/scripts/frontend/stylelint/stylelint-utility-map.js index 7e012b157b3..941198e82a4 100644 --- a/scripts/frontend/stylelint/stylelint-utility-map.js +++ b/scripts/frontend/stylelint/stylelint-utility-map.js @@ -29,7 +29,7 @@ sass.render( // We just use postcss to create a CSS tree postcss([]) .process(cssResult, { - // This supresses a postcss warning + // This suppresses a postcss warning from: undefined, }) .then(result => { diff --git a/spec/controllers/admin/clusters/applications_controller_spec.rb b/spec/controllers/admin/clusters/applications_controller_spec.rb new file mode 100644 index 00000000000..76f261e7d3f --- /dev/null +++ b/spec/controllers/admin/clusters/applications_controller_spec.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Admin::Clusters::ApplicationsController do + include AccessMatchersForController + + def current_application + Clusters::Cluster::APPLICATIONS[application] + end + + shared_examples 'a secure endpoint' do + it { expect { subject }.to be_allowed_for(:admin) } + it { expect { subject }.to be_denied_for(:user) } + it { expect { subject }.to be_denied_for(:external) } + + context 'when instance clusters are disabled' do + before do + stub_feature_flags(instance_clusters: false) + end + + it 'returns 404' do + is_expected.to have_http_status(:not_found) + end + end + end + + let(:cluster) { create(:cluster, :instance, :provided_by_gcp) } + + describe 'POST create' do + subject do + post :create, params: params + end + + let(:application) { 'helm' } + let(:params) { { application: application, id: cluster.id } } + + describe 'functionality' do + let(:admin) { create(:admin) } + + before do + sign_in(admin) + end + + it 'schedule an application installation' do + expect(ClusterInstallAppWorker).to receive(:perform_async).with(application, anything).once + + expect { subject }.to change { current_application.count } + expect(response).to have_http_status(:no_content) + expect(cluster.application_helm).to be_scheduled + end + + context 'when cluster do not exists' do + before do + cluster.destroy! + end + + it 'return 404' do + expect { subject }.not_to change { current_application.count } + expect(response).to have_http_status(:not_found) + end + end + + context 'when application is unknown' do + let(:application) { 'unkwnown-app' } + + it 'return 404' do + is_expected.to have_http_status(:not_found) + end + end + + context 'when application is already installing' do + before do + create(:clusters_applications_helm, :installing, cluster: cluster) + end + + it 'returns 400' do + is_expected.to have_http_status(:bad_request) + end + end + end + + describe 'security' do + before do + allow(ClusterInstallAppWorker).to receive(:perform_async) + end + + it_behaves_like 'a secure endpoint' + end + end + + describe 'PATCH update' do + subject do + patch :update, params: params + end + + let!(:application) { create(:clusters_applications_cert_managers, :installed, cluster: cluster) } + let(:application_name) { application.name } + let(:params) { { application: application_name, id: cluster.id, email: "new-email@example.com" } } + + describe 'functionality' do + let(:admin) { create(:admin) } + + before do + sign_in(admin) + end + + context "when cluster and app exists" do + it "schedules an application update" do + expect(ClusterPatchAppWorker).to receive(:perform_async).with(application.name, anything).once + + is_expected.to have_http_status(:no_content) + + expect(cluster.application_cert_manager).to be_scheduled + end + end + + context 'when cluster do not exists' do + before do + cluster.destroy! + end + + it { is_expected.to have_http_status(:not_found) } + end + + context 'when application is unknown' do + let(:application_name) { 'unkwnown-app' } + + it { is_expected.to have_http_status(:not_found) } + end + + context 'when application is already scheduled' do + before do + application.make_scheduled! + end + + it { is_expected.to have_http_status(:bad_request) } + end + end + + describe 'security' do + before do + allow(ClusterPatchAppWorker).to receive(:perform_async) + end + + it_behaves_like 'a secure endpoint' + end + end +end diff --git a/spec/controllers/admin/clusters_controller_spec.rb b/spec/controllers/admin/clusters_controller_spec.rb new file mode 100644 index 00000000000..7b77cb186a4 --- /dev/null +++ b/spec/controllers/admin/clusters_controller_spec.rb @@ -0,0 +1,540 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Admin::ClustersController do + include AccessMatchersForController + include GoogleApi::CloudPlatformHelpers + + let(:admin) { create(:admin) } + + before do + sign_in(admin) + end + + describe 'GET #index' do + def get_index(params = {}) + get :index, params: params + end + + context 'when feature flag is not enabled' do + before do + stub_feature_flags(instance_clusters: false) + end + + it 'responds with not found' do + get_index + + expect(response).to have_gitlab_http_status(404) + end + end + + context 'when feature flag is enabled' do + before do + stub_feature_flags(instance_clusters: true) + end + + describe 'functionality' do + context 'when instance has one or more clusters' do + let!(:enabled_cluster) do + create(:cluster, :provided_by_gcp, :instance) + end + + let!(:disabled_cluster) do + create(:cluster, :disabled, :provided_by_gcp, :production_environment, :instance) + end + + it 'lists available clusters' do + get_index + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:index) + expect(assigns(:clusters)).to match_array([enabled_cluster, disabled_cluster]) + end + + context 'when page is specified' do + let(:last_page) { Clusters::Cluster.instance_type.page.total_pages } + + before do + allow(Clusters::Cluster).to receive(:paginates_per).and_return(1) + create_list(:cluster, 2, :provided_by_gcp, :production_environment, :instance) + end + + it 'redirects to the page' do + get_index(page: last_page) + + expect(response).to have_gitlab_http_status(:ok) + expect(assigns(:clusters).current_page).to eq(last_page) + end + end + end + + context 'when instance does not have a cluster' do + it 'returns an empty state page' do + get_index + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:index, partial: :empty_state) + expect(assigns(:clusters)).to eq([]) + end + end + end + end + + describe 'security' do + let(:cluster) { create(:cluster, :provided_by_gcp, :instance) } + + it { expect { get_index }.to be_allowed_for(:admin) } + it { expect { get_index }.to be_denied_for(:user) } + it { expect { get_index }.to be_denied_for(:external) } + end + end + + describe 'GET #new' do + def get_new + get :new + end + + describe 'functionality for new cluster' do + context 'when omniauth has been configured' do + let(:key) { 'secret-key' } + let(:session_key_for_redirect_uri) do + GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(key) + end + + before do + allow(SecureRandom).to receive(:hex).and_return(key) + end + + it 'has authorize_url' do + get_new + + expect(assigns(:authorize_url)).to include(key) + expect(session[session_key_for_redirect_uri]).to eq(new_admin_cluster_path) + end + end + + context 'when omniauth has not configured' do + before do + stub_omniauth_setting(providers: []) + end + + it 'does not have authorize_url' do + get_new + + expect(assigns(:authorize_url)).to be_nil + end + end + + context 'when access token is valid' do + before do + stub_google_api_validate_token + end + + it 'has new object' do + get_new + + expect(assigns(:gcp_cluster)).to be_an_instance_of(Clusters::ClusterPresenter) + end + end + + context 'when access token is expired' do + before do + stub_google_api_expired_token + end + + it { expect(@valid_gcp_token).to be_falsey } + end + + context 'when access token is not stored in session' do + it { expect(@valid_gcp_token).to be_falsey } + end + end + + describe 'functionality for existing cluster' do + it 'has new object' do + get_new + + expect(assigns(:user_cluster)).to be_an_instance_of(Clusters::ClusterPresenter) + end + end + + describe 'security' do + it { expect { get_new }.to be_allowed_for(:admin) } + it { expect { get_new }.to be_denied_for(:user) } + it { expect { get_new }.to be_denied_for(:external) } + end + end + + describe 'POST #create_gcp' do + let(:legacy_abac_param) { 'true' } + let(:params) do + { + cluster: { + name: 'new-cluster', + provider_gcp_attributes: { + gcp_project_id: 'gcp-project-12345', + legacy_abac: legacy_abac_param + } + } + } + end + + def post_create_gcp + post :create_gcp, params: params + end + + describe 'functionality' do + context 'when access token is valid' do + before do + stub_google_api_validate_token + end + + it 'creates a new cluster' do + expect(ClusterProvisionWorker).to receive(:perform_async) + expect { post_create_gcp }.to change { Clusters::Cluster.count } + .and change { Clusters::Providers::Gcp.count } + + cluster = Clusters::Cluster.instance_type.first + + expect(response).to redirect_to(admin_cluster_path(cluster)) + expect(cluster).to be_gcp + expect(cluster).to be_kubernetes + expect(cluster.provider_gcp).to be_legacy_abac + end + + context 'when legacy_abac param is false' do + let(:legacy_abac_param) { 'false' } + + it 'creates a new cluster with legacy_abac_disabled' do + expect(ClusterProvisionWorker).to receive(:perform_async) + expect { post_create_gcp }.to change { Clusters::Cluster.count } + .and change { Clusters::Providers::Gcp.count } + expect(Clusters::Cluster.instance_type.first.provider_gcp).not_to be_legacy_abac + end + end + end + + context 'when access token is expired' do + before do + stub_google_api_expired_token + end + + it { expect(@valid_gcp_token).to be_falsey } + end + + context 'when access token is not stored in session' do + it { expect(@valid_gcp_token).to be_falsey } + end + end + + describe 'security' do + before do + allow_any_instance_of(described_class) + .to receive(:token_in_session).and_return('token') + allow_any_instance_of(described_class) + .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s) + allow_any_instance_of(GoogleApi::CloudPlatform::Client) + .to receive(:projects_zones_clusters_create) do + OpenStruct.new( + self_link: 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123', + status: 'RUNNING' + ) + end + + allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil) + end + + it { expect { post_create_gcp }.to be_allowed_for(:admin) } + it { expect { post_create_gcp }.to be_denied_for(:user) } + it { expect { post_create_gcp }.to be_denied_for(:external) } + end + end + + describe 'POST #create_user' do + let(:params) do + { + cluster: { + name: 'new-cluster', + platform_kubernetes_attributes: { + api_url: 'http://my-url', + token: 'test' + } + } + } + end + + def post_create_user + post :create_user, params: params + end + + describe 'functionality' do + context 'when creates a cluster' do + it 'creates a new cluster' do + expect(ClusterProvisionWorker).to receive(:perform_async) + + expect { post_create_user }.to change { Clusters::Cluster.count } + .and change { Clusters::Platforms::Kubernetes.count } + + cluster = Clusters::Cluster.instance_type.first + + expect(response).to redirect_to(admin_cluster_path(cluster)) + expect(cluster).to be_user + expect(cluster).to be_kubernetes + end + end + + context 'when creates a RBAC-enabled cluster' do + let(:params) do + { + cluster: { + name: 'new-cluster', + platform_kubernetes_attributes: { + api_url: 'http://my-url', + token: 'test', + authorization_type: 'rbac' + } + } + } + end + + it 'creates a new cluster' do + expect(ClusterProvisionWorker).to receive(:perform_async) + + expect { post_create_user }.to change { Clusters::Cluster.count } + .and change { Clusters::Platforms::Kubernetes.count } + + cluster = Clusters::Cluster.instance_type.first + + expect(response).to redirect_to(admin_cluster_path(cluster)) + expect(cluster).to be_user + expect(cluster).to be_kubernetes + expect(cluster).to be_platform_kubernetes_rbac + end + end + end + + describe 'security' do + it { expect { post_create_user }.to be_allowed_for(:admin) } + it { expect { post_create_user }.to be_denied_for(:user) } + it { expect { post_create_user }.to be_denied_for(:external) } + end + end + + describe 'GET #cluster_status' do + let(:cluster) { create(:cluster, :providing_by_gcp, :instance) } + + def get_cluster_status + get :cluster_status, + params: { + id: cluster + }, + format: :json + end + + describe 'functionality' do + it 'responds with matching schema' do + get_cluster_status + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('cluster_status') + end + + it 'invokes schedule_status_update on each application' do + expect_any_instance_of(Clusters::Applications::Ingress).to receive(:schedule_status_update) + + get_cluster_status + end + end + + describe 'security' do + it { expect { get_cluster_status }.to be_allowed_for(:admin) } + it { expect { get_cluster_status }.to be_denied_for(:user) } + it { expect { get_cluster_status }.to be_denied_for(:external) } + end + end + + describe 'GET #show' do + let(:cluster) { create(:cluster, :provided_by_gcp, :instance) } + + def get_show + get :show, + params: { + id: cluster + } + end + + describe 'functionality' do + it 'responds successfully' do + get_show + + expect(response).to have_gitlab_http_status(:ok) + expect(assigns(:cluster)).to eq(cluster) + end + end + + describe 'security' do + it { expect { get_show }.to be_allowed_for(:admin) } + it { expect { get_show }.to be_denied_for(:user) } + it { expect { get_show }.to be_denied_for(:external) } + end + end + + describe 'PUT #update' do + def put_update(format: :html) + put :update, params: params.merge( + id: cluster, + format: format + ) + end + + let(:cluster) { create(:cluster, :provided_by_user, :instance) } + let(:domain) { 'test-domain.com' } + + let(:params) do + { + cluster: { + enabled: false, + name: 'my-new-cluster-name', + base_domain: domain + } + } + end + + it 'updates and redirects back to show page' do + put_update + + cluster.reload + expect(response).to redirect_to(admin_cluster_path(cluster)) + expect(flash[:notice]).to eq('Kubernetes cluster was successfully updated.') + expect(cluster.enabled).to be_falsey + expect(cluster.name).to eq('my-new-cluster-name') + expect(cluster.domain).to eq('test-domain.com') + end + + context 'when domain is invalid' do + let(:domain) { 'http://not-a-valid-domain' } + + it 'does not update cluster attributes' do + put_update + + cluster.reload + expect(response).to render_template(:show) + expect(cluster.name).not_to eq('my-new-cluster-name') + expect(cluster.domain).not_to eq('test-domain.com') + end + end + + context 'when format is json' do + context 'when changing parameters' do + context 'when valid parameters are used' do + let(:params) do + { + cluster: { + enabled: false, + name: 'my-new-cluster-name', + domain: domain + } + } + end + + it 'updates and redirects back to show page' do + put_update(format: :json) + + cluster.reload + expect(response).to have_http_status(:no_content) + expect(cluster.enabled).to be_falsey + expect(cluster.name).to eq('my-new-cluster-name') + end + end + + context 'when invalid parameters are used' do + let(:params) do + { + cluster: { + enabled: false, + name: '' + } + } + end + + it 'rejects changes' do + put_update(format: :json) + + expect(response).to have_http_status(:bad_request) + end + end + end + end + + describe 'security' do + set(:cluster) { create(:cluster, :provided_by_gcp, :instance) } + + it { expect { put_update }.to be_allowed_for(:admin) } + it { expect { put_update }.to be_denied_for(:user) } + it { expect { put_update }.to be_denied_for(:external) } + end + end + + describe 'DELETE #destroy' do + let!(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, :instance) } + + def delete_destroy + delete :destroy, + params: { + id: cluster + } + end + + describe 'functionality' do + context 'when cluster is provided by GCP' do + context 'when cluster is created' do + it 'destroys and redirects back to clusters list' do + expect { delete_destroy } + .to change { Clusters::Cluster.count }.by(-1) + .and change { Clusters::Platforms::Kubernetes.count }.by(-1) + .and change { Clusters::Providers::Gcp.count }.by(-1) + + expect(response).to redirect_to(admin_clusters_path) + expect(flash[:notice]).to eq('Kubernetes cluster integration was successfully removed.') + end + end + + context 'when cluster is being created' do + let!(:cluster) { create(:cluster, :providing_by_gcp, :production_environment, :instance) } + + it 'destroys and redirects back to clusters list' do + expect { delete_destroy } + .to change { Clusters::Cluster.count }.by(-1) + .and change { Clusters::Providers::Gcp.count }.by(-1) + + expect(response).to redirect_to(admin_clusters_path) + expect(flash[:notice]).to eq('Kubernetes cluster integration was successfully removed.') + end + end + end + + context 'when cluster is provided by user' do + let!(:cluster) { create(:cluster, :provided_by_user, :production_environment, :instance) } + + it 'destroys and redirects back to clusters list' do + expect { delete_destroy } + .to change { Clusters::Cluster.count }.by(-1) + .and change { Clusters::Platforms::Kubernetes.count }.by(-1) + .and change { Clusters::Providers::Gcp.count }.by(0) + + expect(response).to redirect_to(admin_clusters_path) + expect(flash[:notice]).to eq('Kubernetes cluster integration was successfully removed.') + end + end + end + + describe 'security' do + set(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, :instance) } + + it { expect { delete_destroy }.to be_allowed_for(:admin) } + it { expect { delete_destroy }.to be_denied_for(:user) } + it { expect { delete_destroy }.to be_denied_for(:external) } + end + end +end diff --git a/spec/controllers/concerns/enforces_admin_authentication_spec.rb b/spec/controllers/concerns/enforces_admin_authentication_spec.rb new file mode 100644 index 00000000000..e6a6702fdea --- /dev/null +++ b/spec/controllers/concerns/enforces_admin_authentication_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe EnforcesAdminAuthentication do + let(:user) { create(:user) } + + before do + sign_in(user) + end + + controller(ApplicationController) do + # `described_class` is not available in this context + include EnforcesAdminAuthentication # rubocop:disable RSpec/DescribedClass + + def index + head :ok + end + end + + describe 'authenticate_admin!' do + context 'as an admin' do + let(:user) { create(:admin) } + + it 'renders ok' do + get :index + + expect(response).to have_gitlab_http_status(200) + end + end + + context 'as a user' do + it 'renders a 404' do + get :index + + expect(response).to have_gitlab_http_status(404) + end + end + end +end diff --git a/spec/controllers/concerns/project_unauthorized_spec.rb b/spec/controllers/concerns/project_unauthorized_spec.rb index 57ac00cf4dd..5834b1ef37f 100644 --- a/spec/controllers/concerns/project_unauthorized_spec.rb +++ b/spec/controllers/concerns/project_unauthorized_spec.rb @@ -12,7 +12,7 @@ describe ProjectUnauthorized do render_views - describe '#project_unauthorized_proc' do + describe '.on_routable_not_found' do controller(::Projects::ApplicationController) do def show head :ok diff --git a/spec/controllers/concerns/routable_actions_spec.rb b/spec/controllers/concerns/routable_actions_spec.rb new file mode 100644 index 00000000000..59d48c68b9c --- /dev/null +++ b/spec/controllers/concerns/routable_actions_spec.rb @@ -0,0 +1,156 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe RoutableActions do + controller(::ApplicationController) do + include RoutableActions # rubocop:disable RSpec/DescribedClass + + before_action :routable + + def routable + @klass = params[:type].constantize + @routable = find_routable!(params[:type].constantize, params[:id]) + end + + def show + head :ok + end + end + + def get_routable(routable) + get :show, params: { id: routable.full_path, type: routable.class } + end + + describe '#find_routable!' do + context 'when signed in' do + let(:user) { create(:user) } + + before do + sign_in(user) + end + + context 'with a project' do + let(:routable) { create(:project) } + + context 'when authorized' do + before do + routable.add_guest(user) + end + + it 'returns the project' do + get_routable(routable) + + expect(assigns[:routable]).to be_a(Project) + end + + it 'allows access' do + get_routable(routable) + + expect(response).to have_gitlab_http_status(200) + end + end + + it 'prevents access when not authorized' do + get_routable(routable) + + expect(response).to have_gitlab_http_status(404) + end + end + + context 'with a group' do + let(:routable) { create(:group, :private) } + + context 'when authorized' do + before do + routable.add_guest(user) + end + + it 'returns the group' do + get_routable(routable) + + expect(assigns[:routable]).to be_a(Group) + end + + it 'allows access' do + get_routable(routable) + + expect(response).to have_gitlab_http_status(200) + end + end + + it 'prevents access when not authorized' do + get_routable(routable) + + expect(response).to have_gitlab_http_status(404) + end + end + + context 'with a user' do + let(:routable) { user } + + it 'allows access when authorized' do + get_routable(routable) + + expect(response).to have_gitlab_http_status(200) + end + + it 'prevents access when unauthorized' do + allow(subject).to receive(:can?).and_return(false) + + get_routable(user) + + expect(response).to have_gitlab_http_status(404) + end + end + end + + context 'when not signed in' do + it 'redirects to sign in for private resouces' do + routable = create(:project, :private) + + get_routable(routable) + + expect(response).to have_gitlab_http_status(302) + expect(response.location).to end_with('/users/sign_in') + end + end + end + + describe '#perform_not_found_actions' do + let(:routable) { create(:project) } + + before do + sign_in(create(:user)) + end + + it 'performs multiple checks' do + last_check_called = false + checks = [proc {}, proc { last_check_called = true }] + allow(subject).to receive(:not_found_actions).and_return(checks) + + get_routable(routable) + + expect(last_check_called).to eq(true) + end + + it 'performs checks in the context of the controller' do + check = lambda { |routable| redirect_to routable } + allow(subject).to receive(:not_found_actions).and_return([check]) + + get_routable(routable) + + expect(response.location).to end_with(routable.to_param) + end + + it 'skips checks once one has resulted in a render/redirect' do + first_check = proc { render plain: 'first' } + second_check = proc { render plain: 'second' } + allow(subject).to receive(:not_found_actions).and_return([first_check, second_check]) + + get_routable(routable) + + expect(response.body).to eq('first') + end + end +end diff --git a/spec/controllers/concerns/send_file_upload_spec.rb b/spec/controllers/concerns/send_file_upload_spec.rb index 8408578a7db..a3ce08f736c 100644 --- a/spec/controllers/concerns/send_file_upload_spec.rb +++ b/spec/controllers/concerns/send_file_upload_spec.rb @@ -1,3 +1,4 @@ +# coding: utf-8 # frozen_string_literal: true require 'spec_helper' @@ -13,7 +14,7 @@ describe SendFileUpload do # user/:id def dynamic_segment - File.join(model.class.to_s.underscore, model.id.to_s) + File.join(model.class.underscore, model.id.to_s) end end end diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index cf23d937037..d5eea5b0439 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -383,6 +383,8 @@ describe Projects::EnvironmentsController do end describe 'GET #additional_metrics' do + let(:window_params) { { start: '1554702993.5398998', end: '1554717396.996232' } } + before do allow(controller).to receive(:environment).and_return(environment) end @@ -394,7 +396,7 @@ describe Projects::EnvironmentsController do context 'when requesting metrics as JSON' do it 'returns a metrics JSON document' do - additional_metrics + additional_metrics(window_params) expect(response).to have_gitlab_http_status(204) expect(json_response).to eq({}) @@ -414,23 +416,19 @@ describe Projects::EnvironmentsController do end it 'returns a metrics JSON document' do - additional_metrics + additional_metrics(window_params) expect(response).to be_ok expect(json_response['success']).to be(true) expect(json_response['data']).to eq({}) expect(json_response['last_update']).to eq(42) end + end - context 'when time params are provided' do - it 'returns a metrics JSON document' do - additional_metrics(start: '1554702993.5398998', end: '1554717396.996232') - - expect(response).to be_ok - expect(json_response['success']).to be(true) - expect(json_response['data']).to eq({}) - expect(json_response['last_update']).to eq(42) - end + context 'when time params are missing' do + it 'raises an error when window params are missing' do + expect { additional_metrics } + .to raise_error(ActionController::ParameterMissing) end end @@ -450,7 +448,7 @@ describe Projects::EnvironmentsController do end it 'raises an error when start is missing' do - expect { additional_metrics(start: '1552647300.651094') } + expect { additional_metrics(end: '1552647300.651094') } .to raise_error(ActionController::ParameterMissing) end diff --git a/spec/controllers/projects/graphs_controller_spec.rb b/spec/controllers/projects/graphs_controller_spec.rb index d390e84c9b0..b5248c7f0c8 100644 --- a/spec/controllers/projects/graphs_controller_spec.rb +++ b/spec/controllers/projects/graphs_controller_spec.rb @@ -28,6 +28,21 @@ describe Projects::GraphsController do end describe 'charts' do + context 'with an anonymous user' do + let(:project) { create(:project, :repository, :public) } + + before do + sign_out(user) + end + + it 'renders charts with 200 status code' do + get(:charts, params: { namespace_id: project.namespace.path, project_id: project.path, id: 'master' }) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:charts) + end + end + context 'when languages were previously detected' do let(:project) { create(:project, :repository, detected_repository_languages: true) } let!(:repository_language) { create(:repository_language, project: project) } diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index 088c515c3a6..9a598790ff2 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -46,13 +46,17 @@ describe RegistrationsController do end context 'when reCAPTCHA is enabled' do + def fail_recaptcha + # Without this, `verify_recaptcha` arbitrarily returns true in test env + Recaptcha.configuration.skip_verify_env.delete('test') + end + before do stub_application_setting(recaptcha_enabled: true) end it 'displays an error when the reCAPTCHA is not solved' do - # Without this, `verify_recaptcha` arbitrarily returns true in test env - Recaptcha.configuration.skip_verify_env.delete('test') + fail_recaptcha post(:create, params: user_params) @@ -70,6 +74,17 @@ describe RegistrationsController do expect(flash[:notice]).to include 'Welcome! You have signed up successfully.' end + + it 'does not require reCAPTCHA if disabled by feature flag' do + stub_feature_flags(registrations_recaptcha: false) + fail_recaptcha + + post(:create, params: user_params) + + expect(controller).not_to receive(:verify_recaptcha) + expect(flash[:alert]).to be_nil + expect(flash[:notice]).to include 'Welcome! You have signed up successfully.' + end end context 'when terms are enforced' do diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index ab185ab3972..743ec322885 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -260,6 +260,7 @@ FactoryBot.define do trait(:merge_requests_enabled) { merge_requests_access_level ProjectFeature::ENABLED } trait(:merge_requests_disabled) { merge_requests_access_level ProjectFeature::DISABLED } trait(:merge_requests_private) { merge_requests_access_level ProjectFeature::PRIVATE } + trait(:merge_requests_public) { merge_requests_access_level ProjectFeature::PUBLIC } trait(:repository_enabled) { repository_access_level ProjectFeature::ENABLED } trait(:repository_disabled) { repository_access_level ProjectFeature::DISABLED } trait(:repository_private) { repository_access_level ProjectFeature::PRIVATE } diff --git a/spec/factories/uploads.rb b/spec/factories/uploads.rb index 7256f785e1f..426abdc2a6c 100644 --- a/spec/factories/uploads.rb +++ b/spec/factories/uploads.rb @@ -13,7 +13,7 @@ FactoryBot.define do end # this needs to comply with RecordsUpload::Concern#upload_path - path { File.join("uploads/-/system", model.class.to_s.underscore, mount_point.to_s, 'avatar.jpg') } + path { File.join("uploads/-/system", model.class.underscore, mount_point.to_s, 'avatar.jpg') } trait :personal_snippet_upload do uploader "PersonalFileUploader" diff --git a/spec/features/admin/admin_sees_project_statistics_spec.rb b/spec/features/admin/admin_sees_project_statistics_spec.rb new file mode 100644 index 00000000000..95d1fc5b57a --- /dev/null +++ b/spec/features/admin/admin_sees_project_statistics_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe "Admin > Admin sees project statistics" do + let(:current_user) { create(:admin) } + + before do + sign_in(current_user) + + visit admin_project_path(project) + end + + context 'when project has statistics' do + let(:project) { create(:project, :repository) } + + it "shows project statistics" do + expect(page).to have_content("Storage: 0 Bytes (0 Bytes repositories, 0 Bytes build artifacts, 0 Bytes LFS)") + end + end + + context 'when project has no statistics' do + let(:project) { create(:project, :repository) { |project| project.statistics.destroy } } + + it "shows 'Storage: Unknown'" do + expect(page).to have_content("Storage: Unknown") + end + end +end diff --git a/spec/features/admin/admin_sees_projects_statistics_spec.rb b/spec/features/admin/admin_sees_projects_statistics_spec.rb new file mode 100644 index 00000000000..6a6f369ac7c --- /dev/null +++ b/spec/features/admin/admin_sees_projects_statistics_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe "Admin > Admin sees projects statistics" do + let(:current_user) { create(:admin) } + + before do + create(:project, :repository) + create(:project, :repository) { |project| project.statistics.destroy } + + sign_in(current_user) + + visit admin_projects_path + end + + it "shows project statistics for projects that have them" do + expect(page.all('.stats').map(&:text)).to contain_exactly("0 Bytes", "Unknown") + end +end diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb index 9d1c1e3acc7..d1ed64cce7f 100644 --- a/spec/features/dashboard/projects_spec.rb +++ b/spec/features/dashboard/projects_spec.rb @@ -112,6 +112,14 @@ describe 'Dashboard Projects' do expect(first('.project-row')).to have_content(project_with_most_stars.title) end + + it 'shows tabs to filter by all projects or personal' do + visit dashboard_projects_path + segmented_button = page.find('.filtered-search-nav .button-filter-group') + + expect(segmented_button).to have_content 'All' + expect(segmented_button).to have_content 'Personal' + end end context 'when on Starred projects tab', :js do @@ -134,6 +142,12 @@ describe 'Dashboard Projects' do expect(find('.nav-links li:nth-child(1) .badge-pill')).to have_content(1) expect(find('.nav-links li:nth-child(2) .badge-pill')).to have_content(1) end + + it 'does not show tabs to filter by all projects or personal' do + visit(starred_dashboard_projects_path) + + expect(page).not_to have_content '.filtered-search-nav' + end end describe 'with a pipeline', :clean_gitlab_redis_shared_state do diff --git a/spec/features/dashboard/user_filters_projects_spec.rb b/spec/features/dashboard/user_filters_projects_spec.rb index cc86114e436..4410c8f887f 100644 --- a/spec/features/dashboard/user_filters_projects_spec.rb +++ b/spec/features/dashboard/user_filters_projects_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' describe 'Dashboard > User filters projects' do let(:user) { create(:user) } - let(:project) { create(:project, name: 'Victorialand', namespace: user.namespace) } + let(:project) { create(:project, name: 'Victorialand', namespace: user.namespace, created_at: 2.seconds.ago, updated_at: 2.seconds.ago) } let(:user2) { create(:user) } - let(:project2) { create(:project, name: 'Treasure', namespace: user2.namespace) } + let(:project2) { create(:project, name: 'Treasure', namespace: user2.namespace, created_at: 1.second.ago, updated_at: 1.second.ago) } before do project.add_maintainer(user) @@ -14,6 +14,7 @@ describe 'Dashboard > User filters projects' do describe 'filtering personal projects' do before do + stub_feature_flags(project_list_filter_bar: false) project2.add_developer(user) visit dashboard_projects_path @@ -30,6 +31,7 @@ describe 'Dashboard > User filters projects' do describe 'filtering starred projects', :js do before do + stub_feature_flags(project_list_filter_bar: false) user.toggle_star(project) visit dashboard_projects_path @@ -42,4 +44,187 @@ describe 'Dashboard > User filters projects' do expect(page).not_to have_content('You don\'t have starred projects yet') end end + + describe 'without search bar', :js do + before do + stub_feature_flags(project_list_filter_bar: false) + + project2.add_developer(user) + visit dashboard_projects_path + end + + it 'autocompletes searches upon typing', :js do + expect(page).to have_content 'Victorialand' + expect(page).to have_content 'Treasure' + + fill_in 'project-filter-form-field', with: 'Lord beerus\n' + + expect(page).not_to have_content 'Victorialand' + expect(page).not_to have_content 'Treasure' + end + end + + describe 'with search bar', :js do + before do + stub_feature_flags(project_list_filter_bar: true) + + project2.add_developer(user) + visit dashboard_projects_path + end + + # TODO: move these helpers somewhere more useful + def click_sort_direction + page.find('.filtered-search-block #filtered-search-sorting-dropdown .reverse-sort-btn').click + end + + def select_dropdown_option(selector, label) + dropdown = page.find(selector) + dropdown.click + + dropdown.find('.dropdown-menu a', text: label, match: :first).click + end + + def expect_to_see_projects(sorted_projects) + list = page.all('.projects-list .project-name').map(&:text) + expect(list).to match(sorted_projects) + end + + describe 'Search' do + it 'executes when the search button is clicked' do + expect(page).to have_content 'Victorialand' + expect(page).to have_content 'Treasure' + + fill_in 'project-filter-form-field', with: 'Lord vegeta\n' + find('.filtered-search .btn').click + + expect(page).not_to have_content 'Victorialand' + expect(page).not_to have_content 'Treasure' + end + + it 'will execute when i press enter' do + expect(page).to have_content 'Victorialand' + expect(page).to have_content 'Treasure' + + fill_in 'project-filter-form-field', with: 'Lord frieza\n' + find('#project-filter-form-field').native.send_keys :enter + + expect(page).not_to have_content 'Victorialand' + expect(page).not_to have_content 'Treasure' + end + end + + describe 'Filter' do + before do + private_project = create(:project, :private, name: 'Private project', namespace: user.namespace) + internal_project = create(:project, :internal, name: 'Internal project', namespace: user.namespace) + + private_project.add_maintainer(user) + internal_project.add_maintainer(user) + end + + it 'filters private projects only' do + select_dropdown_option '#filtered-search-visibility-dropdown', 'Private' + + expect(current_url).to match(/visibility_level=0/) + + list = page.all('.projects-list .project-name').map(&:text) + + expect(list).to contain_exactly("Private project", "Treasure", "Victorialand") + end + + it 'filters internal projects only' do + select_dropdown_option '#filtered-search-visibility-dropdown', 'Internal' + + expect(current_url).to match(/visibility_level=10/) + + list = page.all('.projects-list .project-name').map(&:text) + + expect(list).to contain_exactly('Internal project') + end + + it 'filters any project' do + select_dropdown_option '#filtered-search-visibility-dropdown', 'Any' + list = page.all('.projects-list .project-name').map(&:text) + + expect(list).to contain_exactly("Internal project", "Private project", "Treasure", "Victorialand") + end + end + + describe 'Sorting' do + let(:desc_sorted_project_names) { %w[Treasure Victorialand] } + + before do + user.toggle_star(project) + user.toggle_star(project2) + user2.toggle_star(project2) + end + + it 'has all sorting options', :js do + sorting_dropdown = page.find('.filtered-search-block #filtered-search-sorting-dropdown') + + expect(sorting_dropdown).to have_css '.reverse-sort-btn' + + sorting_dropdown.click + + ['Last updated', 'Created date', 'Name', 'Stars'].each do |label| + expect(sorting_dropdown).to have_content(label) + end + end + + it 'defaults to "Last updated"', :js do + page.find('.filtered-search-block #filtered-search-sorting-dropdown').click + active_sorting_option = page.first('.filtered-search-block #filtered-search-sorting-dropdown .is-active') + + expect(active_sorting_option).to have_content 'Last updated' + end + + context 'Sorting by name' do + it 'sorts the project list' do + select_dropdown_option '#filtered-search-sorting-dropdown', 'Name' + + expect_to_see_projects(desc_sorted_project_names) + + click_sort_direction + + expect_to_see_projects(desc_sorted_project_names.reverse) + end + end + + context 'Sorting by Last updated' do + it 'sorts the project list' do + select_dropdown_option '#filtered-search-sorting-dropdown', 'Last updated' + + expect_to_see_projects(desc_sorted_project_names) + + click_sort_direction + + expect_to_see_projects(desc_sorted_project_names.reverse) + end + end + + context 'Sorting by Created date' do + it 'sorts the project list' do + select_dropdown_option '#filtered-search-sorting-dropdown', 'Created date' + + expect_to_see_projects(desc_sorted_project_names) + + click_sort_direction + + expect_to_see_projects(desc_sorted_project_names.reverse) + end + end + + context 'Sorting by Stars' do + it 'sorts the project list' do + select_dropdown_option '#filtered-search-sorting-dropdown', 'Stars' + + expect_to_see_projects(desc_sorted_project_names) + + click_sort_direction + + expect_to_see_projects(desc_sorted_project_names.reverse) + end + end + end + end end diff --git a/spec/features/issuables/markdown_references/jira_spec.rb b/spec/features/issuables/markdown_references/jira_spec.rb index cc04798248c..8eaccfc0949 100644 --- a/spec/features/issuables/markdown_references/jira_spec.rb +++ b/spec/features/issuables/markdown_references/jira_spec.rb @@ -1,6 +1,6 @@ require "rails_helper" -describe "Jira", :js, :quarantine do +describe "Jira", :js do let(:user) { create(:user) } let(:actual_project) { create(:project, :public, :repository) } let(:merge_request) { create(:merge_request, target_project: actual_project, source_project: actual_project) } diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb index 40ba676ff92..a32c6bdcf8f 100644 --- a/spec/features/merge_request/user_sees_merge_widget_spec.rb +++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb @@ -670,4 +670,26 @@ describe 'Merge request > User sees merge widget', :js do end end end + + context 'when MR has pipeline but user does not have permission' do + let(:sha) { project.commit(merge_request.source_branch).sha } + let!(:pipeline) { create(:ci_pipeline_without_jobs, status: 'success', sha: sha, project: project, ref: merge_request.source_branch) } + + before do + project.update( + visibility_level: Gitlab::VisibilityLevel::PUBLIC, + public_builds: false + ) + merge_request.update!(head_pipeline: pipeline) + sign_out(:user) + + visit project_merge_request_path(project, merge_request) + end + + it 'renders a CI pipeline error' do + within '.ci-widget' do + expect(page).to have_content('Could not retrieve the pipeline status.') + end + end + end end diff --git a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb index 1b5dd6945e0..04c7f4b6c76 100644 --- a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb +++ b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb @@ -121,7 +121,7 @@ describe 'User comments on a diff', :js do end context 'multi-line suggestions' do - it 'suggestion is presented' do + before do click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']")) page.within('.js-discussion-note-form') do @@ -130,7 +130,9 @@ describe 'User comments on a diff', :js do end wait_for_requests + end + it 'suggestion is presented' do page.within('.diff-discussions') do expect(page).to have_button('Apply suggestion') expect(page).to have_content('Suggested change') @@ -160,22 +162,24 @@ describe 'User comments on a diff', :js do end it 'suggestion is appliable' do - click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']")) + page.within('.diff-discussions') do + expect(page).not_to have_content('Applied') - page.within('.js-discussion-note-form') do - fill_in('note_note', with: "```suggestion:-3+5\n# change to a\n# comment\n# with\n# broken\n# lines\n```") - click_button('Comment') - end + click_button('Apply suggestion') + wait_for_requests - wait_for_requests + expect(page).to have_content('Applied') + end + end + it 'resolves discussion when applied' do page.within('.diff-discussions') do - expect(page).not_to have_content('Applied') + expect(page).not_to have_content('Unresolve discussion') click_button('Apply suggestion') wait_for_requests - expect(page).to have_content('Applied') + expect(page).to have_content('Unresolve discussion') end end end diff --git a/spec/features/oauth_login_spec.rb b/spec/features/oauth_login_spec.rb index f4105730402..5ebfc32952d 100644 --- a/spec/features/oauth_login_spec.rb +++ b/spec/features/oauth_login_spec.rb @@ -14,7 +14,7 @@ describe 'OAuth Login', :js, :allow_forgery_protection do end providers = [:github, :twitter, :bitbucket, :gitlab, :google_oauth2, - :facebook, :cas3, :auth0, :authentiq] + :facebook, :cas3, :auth0, :authentiq, :salesforce] before(:all) do # The OmniAuth `full_host` parameter doesn't get set correctly (it gets set to something like `http://localhost` diff --git a/spec/features/profiles/user_edit_preferences_spec.rb b/spec/features/profiles/user_edit_preferences_spec.rb new file mode 100644 index 00000000000..2d2da222998 --- /dev/null +++ b/spec/features/profiles/user_edit_preferences_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe 'User edit preferences profile' do + let(:user) { create(:user) } + + before do + stub_feature_flags(user_time_settings: true) + sign_in(user) + visit(profile_preferences_path) + end + + it 'allows the user to toggle their time format preference' do + field = page.find_field("user[time_format_in_24h]") + + expect(field).not_to be_checked + + field.click + + expect(field).to be_checked + end + + it 'allows the user to toggle their time display preference' do + field = page.find_field("user[time_display_relative]") + + expect(field).to be_checked + + field.click + + expect(field).not_to be_checked + end +end diff --git a/spec/features/profiles/user_edit_profile_spec.rb b/spec/features/profiles/user_edit_profile_spec.rb index b43711f6ef6..a53da94ef7d 100644 --- a/spec/features/profiles/user_edit_profile_spec.rb +++ b/spec/features/profiles/user_edit_profile_spec.rb @@ -327,5 +327,37 @@ describe 'User edit profile' do end end end + + context 'User time preferences', :js do + let(:issue) { create(:issue, project: project)} + let(:project) { create(:project) } + + before do + stub_feature_flags(user_time_settings: true) + end + + it 'shows the user time preferences form' do + expect(page).to have_content('Time settings') + end + + it 'allows the user to select a time zone from a dropdown list of options' do + expect(page.find('.user-time-preferences .dropdown')).not_to have_css('.show') + + page.find('.user-time-preferences .js-timezone-dropdown').click + + expect(page.find('.user-time-preferences .dropdown')).to have_css('.show') + + page.find("a", text: "Nuku'alofa").click + + tz = page.find('.user-time-preferences #user_timezone', visible: false) + + expect(tz.value).to eq('Pacific/Tongatapu') + end + + it 'timezone defaults to servers default' do + timezone_name = Time.zone.tzinfo.name + expect(page.find('.user-time-preferences #user_timezone', visible: false).value).to eq(timezone_name) + 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 index b6dbf76bc9b..51c884201a6 100644 --- a/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb +++ b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb @@ -5,6 +5,8 @@ describe 'Projects > Files > User views files page' do let(:user) { project.owner } before do + stub_feature_flags(vue_file_list: false) + sign_in user visit project_tree_path(project, project.repository.root_ref) end diff --git a/spec/features/projects/files/user_browses_files_spec.rb b/spec/features/projects/files/user_browses_files_spec.rb index 66268355345..a5d849db8a3 100644 --- a/spec/features/projects/files/user_browses_files_spec.rb +++ b/spec/features/projects/files/user_browses_files_spec.rb @@ -11,6 +11,7 @@ describe "User browses files" do let(:user) { project.owner } before do + stub_feature_flags(vue_file_list: false) stub_feature_flags(csslab: false) sign_in(user) end diff --git a/spec/features/projects/files/user_browses_lfs_files_spec.rb b/spec/features/projects/files/user_browses_lfs_files_spec.rb index d56476adb05..d5cb8f9212d 100644 --- a/spec/features/projects/files/user_browses_lfs_files_spec.rb +++ b/spec/features/projects/files/user_browses_lfs_files_spec.rb @@ -5,6 +5,8 @@ describe 'Projects > Files > User browses LFS files' do let(:user) { project.owner } before do + stub_feature_flags(vue_file_list: false) + sign_in(user) end diff --git a/spec/features/projects/files/user_creates_directory_spec.rb b/spec/features/projects/files/user_creates_directory_spec.rb index 847b5f0860f..e29e867492e 100644 --- a/spec/features/projects/files/user_creates_directory_spec.rb +++ b/spec/features/projects/files/user_creates_directory_spec.rb @@ -11,6 +11,8 @@ describe 'Projects > Files > User creates a directory', :js do let(:user) { create(:user) } before do + stub_feature_flags(vue_file_list: false) + project.add_developer(user) sign_in(user) visit project_tree_path(project, 'master') diff --git a/spec/features/projects/files/user_deletes_files_spec.rb b/spec/features/projects/files/user_deletes_files_spec.rb index 614b11fa5c8..11ee87f245b 100644 --- a/spec/features/projects/files/user_deletes_files_spec.rb +++ b/spec/features/projects/files/user_deletes_files_spec.rb @@ -12,6 +12,8 @@ describe 'Projects > Files > User deletes files', :js do let(:user) { create(:user) } before do + stub_feature_flags(vue_file_list: false) + sign_in(user) end diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb index 2de22582b2c..26efb5e6787 100644 --- a/spec/features/projects/files/user_edits_files_spec.rb +++ b/spec/features/projects/files/user_edits_files_spec.rb @@ -10,6 +10,7 @@ describe 'Projects > Files > User edits files', :js do before do stub_feature_flags(web_ide_default: false) + stub_feature_flags(vue_file_list: false) sign_in(user) end diff --git a/spec/features/projects/files/user_replaces_files_spec.rb b/spec/features/projects/files/user_replaces_files_spec.rb index e3da28d73c3..bfd612e4cc8 100644 --- a/spec/features/projects/files/user_replaces_files_spec.rb +++ b/spec/features/projects/files/user_replaces_files_spec.rb @@ -14,6 +14,8 @@ describe 'Projects > Files > User replaces files', :js do let(:user) { create(:user) } before do + stub_feature_flags(vue_file_list: false) + sign_in(user) end diff --git a/spec/features/projects/files/user_uploads_files_spec.rb b/spec/features/projects/files/user_uploads_files_spec.rb index af3fc528a20..25ff3fdf411 100644 --- a/spec/features/projects/files/user_uploads_files_spec.rb +++ b/spec/features/projects/files/user_uploads_files_spec.rb @@ -14,6 +14,8 @@ describe 'Projects > Files > User uploads files' do let(:project2_tree_path_root_ref) { project_tree_path(project2, project2.repository.root_ref) } before do + stub_feature_flags(vue_file_list: false) + project.add_maintainer(user) sign_in(user) end diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb index f7de769cee9..8c7bc192c50 100644 --- a/spec/features/projects/settings/repository_settings_spec.rb +++ b/spec/features/projects/settings/repository_settings_spec.rb @@ -236,5 +236,17 @@ describe 'Projects > Settings > Repository settings' do expect(mirrored_project.remote_mirrors.count).to eq(0) end end + + it 'shows a disabled mirror' do + create(:remote_mirror, project: project, enabled: false) + + visit project_settings_repository_path(project) + + mirror = find('.qa-mirrored-repository-row') + + expect(mirror).to have_selector('.qa-delete-mirror') + expect(mirror).to have_selector('.qa-disabled-mirror-badge') + expect(mirror).not_to have_selector('.qa-update-now-button') + end end end diff --git a/spec/features/projects/tree/tree_show_spec.rb b/spec/features/projects/tree/tree_show_spec.rb index 45e81e1c040..3ccea2db705 100644 --- a/spec/features/projects/tree/tree_show_spec.rb +++ b/spec/features/projects/tree/tree_show_spec.rb @@ -8,6 +8,7 @@ describe 'Projects tree', :js do let(:test_sha) { '7975be0116940bf2ad4321f79d02a55c5f7779aa' } before do + stub_feature_flags(vue_file_list: false) project.add_maintainer(user) sign_in(user) end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index ff4e6197746..4fe45311b2d 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -5,6 +5,7 @@ describe 'Project' do include MobileHelpers before do + stub_feature_flags(vue_file_list: false) stub_feature_flags(approval_rules: false) end diff --git a/spec/finders/cluster_ancestors_finder_spec.rb b/spec/finders/cluster_ancestors_finder_spec.rb index 332086c42e2..750042b6b54 100644 --- a/spec/finders/cluster_ancestors_finder_spec.rb +++ b/spec/finders/cluster_ancestors_finder_spec.rb @@ -8,11 +8,15 @@ describe ClusterAncestorsFinder, '#execute' do let(:user) { create(:user) } let!(:project_cluster) do - create(:cluster, :provided_by_user, cluster_type: :project_type, projects: [project]) + create(:cluster, :provided_by_user, :project, projects: [project]) end let!(:group_cluster) do - create(:cluster, :provided_by_user, cluster_type: :group_type, groups: [group]) + create(:cluster, :provided_by_user, :group, groups: [group]) + end + + let!(:instance_cluster) do + create(:cluster, :provided_by_user, :instance) end subject { described_class.new(clusterable, user).execute } @@ -25,7 +29,7 @@ describe ClusterAncestorsFinder, '#execute' do end it 'returns the project clusters followed by group clusters' do - is_expected.to eq([project_cluster, group_cluster]) + is_expected.to eq([project_cluster, group_cluster, instance_cluster]) end context 'nested groups', :nested_groups do @@ -33,11 +37,11 @@ describe ClusterAncestorsFinder, '#execute' do let(:parent_group) { create(:group) } let!(:parent_group_cluster) do - create(:cluster, :provided_by_user, cluster_type: :group_type, groups: [parent_group]) + create(:cluster, :provided_by_user, :group, groups: [parent_group]) end it 'returns the project clusters followed by group clusters ordered ascending the hierarchy' do - is_expected.to eq([project_cluster, group_cluster, parent_group_cluster]) + is_expected.to eq([project_cluster, group_cluster, parent_group_cluster, instance_cluster]) end end end @@ -58,7 +62,7 @@ describe ClusterAncestorsFinder, '#execute' do end it 'returns the list of group clusters' do - is_expected.to eq([group_cluster]) + is_expected.to eq([group_cluster, instance_cluster]) end context 'nested groups', :nested_groups do @@ -66,12 +70,21 @@ describe ClusterAncestorsFinder, '#execute' do let(:parent_group) { create(:group) } let!(:parent_group_cluster) do - create(:cluster, :provided_by_user, cluster_type: :group_type, groups: [parent_group]) + create(:cluster, :provided_by_user, :group, groups: [parent_group]) end it 'returns the list of group clusters ordered ascending the hierarchy' do - is_expected.to eq([group_cluster, parent_group_cluster]) + is_expected.to eq([group_cluster, parent_group_cluster, instance_cluster]) end end end + + context 'for an instance' do + let(:clusterable) { Clusters::Instance.new } + let(:user) { create(:admin) } + + it 'returns the list of instance clusters' do + is_expected.to eq([instance_cluster]) + end + end end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 6a47cd013f8..89fdaceaa9f 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -641,9 +641,7 @@ describe IssuesFinder do end it 'filters by confidentiality' do - expect(Issue).to receive(:where).with(a_string_matching('confidential'), anything) - - subject + expect(subject.to_sql).to match("issues.confidential") end end @@ -660,9 +658,7 @@ describe IssuesFinder do end it 'filters by confidentiality' do - expect(Issue).to receive(:where).with(a_string_matching('confidential'), anything) - - subject + expect(subject.to_sql).to match("issues.confidential") end end diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 117f4a03735..da5e9dab058 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -31,7 +31,7 @@ describe MergeRequestsFinder do end context 'filtering by group' do - it 'includes all merge requests when user has access exceluding merge requests from projects the user does not have access to' do + it 'includes all merge requests when user has access excluding merge requests from projects the user does not have access to' do private_project = allow_gitaly_n_plus_1 { create(:project, :private, group: group) } private_project.add_guest(user) create(:merge_request, :simple, author: user, source_project: private_project, target_project: private_project) diff --git a/spec/fixtures/api/schemas/entities/test_case.json b/spec/fixtures/api/schemas/entities/test_case.json index c9ba1f3ad18..70f6edeeeb7 100644 --- a/spec/fixtures/api/schemas/entities/test_case.json +++ b/spec/fixtures/api/schemas/entities/test_case.json @@ -7,6 +7,7 @@ "properties": { "status": { "type": "string" }, "name": { "type": "string" }, + "classname": { "type": "string" }, "execution_time": { "type": "float" }, "system_output": { "type": ["string", "null"] }, "stack_trace": { "type": ["string", "null"] } diff --git a/spec/fixtures/api/schemas/public_api/v4/label_basic.json b/spec/fixtures/api/schemas/public_api/v4/label_basic.json new file mode 100644 index 00000000000..37bbdcb14fe --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/label_basic.json @@ -0,0 +1,24 @@ +{ + "type": "object", + "required": [ + "id", + "name", + "color", + "description", + "text_color" + ], + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{3}{1,2}$" + }, + "description": { "type": ["string", "null"] }, + "text_color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{3}{1,2}$" + } + }, + "additionalProperties": false +} diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js new file mode 100644 index 00000000000..d23393db60d --- /dev/null +++ b/spec/frontend/boards/stores/actions_spec.js @@ -0,0 +1,67 @@ +import actions from '~/boards/stores/actions'; + +const expectNotImplemented = action => { + it('is not implemented', () => { + expect(action).toThrow(new Error('Not implemented!')); + }); +}; + +describe('setEndpoints', () => { + expectNotImplemented(actions.setEndpoints); +}); + +describe('fetchLists', () => { + expectNotImplemented(actions.fetchLists); +}); + +describe('generateDefaultLists', () => { + expectNotImplemented(actions.generateDefaultLists); +}); + +describe('createList', () => { + expectNotImplemented(actions.createList); +}); + +describe('updateList', () => { + expectNotImplemented(actions.updateList); +}); + +describe('deleteList', () => { + expectNotImplemented(actions.deleteList); +}); + +describe('fetchIssuesForList', () => { + expectNotImplemented(actions.fetchIssuesForList); +}); + +describe('moveIssue', () => { + expectNotImplemented(actions.moveIssue); +}); + +describe('createNewIssue', () => { + expectNotImplemented(actions.createNewIssue); +}); + +describe('fetchBacklog', () => { + expectNotImplemented(actions.fetchBacklog); +}); + +describe('bulkUpdateIssues', () => { + expectNotImplemented(actions.bulkUpdateIssues); +}); + +describe('fetchIssue', () => { + expectNotImplemented(actions.fetchIssue); +}); + +describe('toggleIssueSubscription', () => { + expectNotImplemented(actions.toggleIssueSubscription); +}); + +describe('showPage', () => { + expectNotImplemented(actions.showPage); +}); + +describe('toggleEmptyState', () => { + expectNotImplemented(actions.toggleEmptyState); +}); diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js new file mode 100644 index 00000000000..aa477766978 --- /dev/null +++ b/spec/frontend/boards/stores/mutations_spec.js @@ -0,0 +1,91 @@ +import mutations from '~/boards/stores/mutations'; + +const expectNotImplemented = action => { + it('is not implemented', () => { + expect(action).toThrow(new Error('Not implemented!')); + }); +}; + +describe('SET_ENDPOINTS', () => { + expectNotImplemented(mutations.SET_ENDPOINTS); +}); + +describe('REQUEST_ADD_LIST', () => { + expectNotImplemented(mutations.REQUEST_ADD_LIST); +}); + +describe('RECEIVE_ADD_LIST_SUCCESS', () => { + expectNotImplemented(mutations.RECEIVE_ADD_LIST_SUCCESS); +}); + +describe('RECEIVE_ADD_LIST_ERROR', () => { + expectNotImplemented(mutations.RECEIVE_ADD_LIST_ERROR); +}); + +describe('REQUEST_UPDATE_LIST', () => { + expectNotImplemented(mutations.REQUEST_UPDATE_LIST); +}); + +describe('RECEIVE_UPDATE_LIST_SUCCESS', () => { + expectNotImplemented(mutations.RECEIVE_UPDATE_LIST_SUCCESS); +}); + +describe('RECEIVE_UPDATE_LIST_ERROR', () => { + expectNotImplemented(mutations.RECEIVE_UPDATE_LIST_ERROR); +}); + +describe('REQUEST_REMOVE_LIST', () => { + expectNotImplemented(mutations.REQUEST_REMOVE_LIST); +}); + +describe('RECEIVE_REMOVE_LIST_SUCCESS', () => { + expectNotImplemented(mutations.RECEIVE_REMOVE_LIST_SUCCESS); +}); + +describe('RECEIVE_REMOVE_LIST_ERROR', () => { + expectNotImplemented(mutations.RECEIVE_REMOVE_LIST_ERROR); +}); + +describe('REQUEST_ADD_ISSUE', () => { + expectNotImplemented(mutations.REQUEST_ADD_ISSUE); +}); + +describe('RECEIVE_ADD_ISSUE_SUCCESS', () => { + expectNotImplemented(mutations.RECEIVE_ADD_ISSUE_SUCCESS); +}); + +describe('RECEIVE_ADD_ISSUE_ERROR', () => { + expectNotImplemented(mutations.RECEIVE_ADD_ISSUE_ERROR); +}); + +describe('REQUEST_MOVE_ISSUE', () => { + expectNotImplemented(mutations.REQUEST_MOVE_ISSUE); +}); + +describe('RECEIVE_MOVE_ISSUE_SUCCESS', () => { + expectNotImplemented(mutations.RECEIVE_MOVE_ISSUE_SUCCESS); +}); + +describe('RECEIVE_MOVE_ISSUE_ERROR', () => { + expectNotImplemented(mutations.RECEIVE_MOVE_ISSUE_ERROR); +}); + +describe('REQUEST_UPDATE_ISSUE', () => { + expectNotImplemented(mutations.REQUEST_UPDATE_ISSUE); +}); + +describe('RECEIVE_UPDATE_ISSUE_SUCCESS', () => { + expectNotImplemented(mutations.RECEIVE_UPDATE_ISSUE_SUCCESS); +}); + +describe('RECEIVE_UPDATE_ISSUE_ERROR', () => { + expectNotImplemented(mutations.RECEIVE_UPDATE_ISSUE_ERROR); +}); + +describe('SET_CURRENT_PAGE', () => { + expectNotImplemented(mutations.SET_CURRENT_PAGE); +}); + +describe('TOGGLE_EMPTY_STATE', () => { + expectNotImplemented(mutations.TOGGLE_EMPTY_STATE); +}); diff --git a/spec/frontend/boards/stores/state_spec.js b/spec/frontend/boards/stores/state_spec.js new file mode 100644 index 00000000000..35490a63567 --- /dev/null +++ b/spec/frontend/boards/stores/state_spec.js @@ -0,0 +1,11 @@ +import createState from '~/boards/stores/state'; + +describe('createState', () => { + it('is a function', () => { + expect(createState).toEqual(expect.any(Function)); + }); + + it('returns an object', () => { + expect(createState()).toEqual(expect.any(Object)); + }); +}); diff --git a/spec/frontend/environment.js b/spec/frontend/environment.js index 34df8019a2e..a8c8688441d 100644 --- a/spec/frontend/environment.js +++ b/spec/frontend/environment.js @@ -24,8 +24,9 @@ class CustomEnvironment extends JSDOMEnvironment { }); const { testEnvironmentOptions } = config; + const { IS_EE } = testEnvironmentOptions; this.global.gon = { - ee: testEnvironmentOptions.IS_EE, + ee: IS_EE, }; this.rejectedPromises = []; @@ -33,6 +34,20 @@ class CustomEnvironment extends JSDOMEnvironment { this.global.promiseRejectionHandler = error => { this.rejectedPromises.push(error); }; + + this.global.fixturesBasePath = `${process.cwd()}/${ + IS_EE ? 'ee/' : '' + }spec/javascripts/fixtures`; + + // Not yet supported by JSDOM: https://github.com/jsdom/jsdom/issues/317 + this.global.document.createRange = () => ({ + setStart: () => {}, + setEnd: () => {}, + commonAncestorContainer: { + nodeName: 'BODY', + ownerDocument: this.global.document, + }, + }); } async teardown() { diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js index 3886853f3c1..8af49fd47a2 100644 --- a/spec/frontend/gfm_auto_complete_spec.js +++ b/spec/frontend/gfm_auto_complete_spec.js @@ -7,11 +7,8 @@ import 'jquery.caret'; import 'at.js'; import { TEST_HOST } from 'helpers/test_constants'; -import { setTestTimeout } from 'helpers/timeout'; import { getJSONFixture } from 'helpers/fixtures'; -setTestTimeout(500); - const labelsFixture = getJSONFixture('autocomplete_sources/labels.json'); describe('GfmAutoComplete', () => { diff --git a/spec/frontend/helpers/fixtures.js b/spec/frontend/helpers/fixtures.js index f0351aa31c6..b77bcd6266e 100644 --- a/spec/frontend/helpers/fixtures.js +++ b/spec/frontend/helpers/fixtures.js @@ -3,10 +3,8 @@ import path from 'path'; import { ErrorWithStack } from 'jest-util'; -const fixturesBasePath = path.join(process.cwd(), 'spec', 'javascripts', 'fixtures'); - export function getFixture(relativePath) { - const absolutePath = path.join(fixturesBasePath, relativePath); + const absolutePath = path.join(global.fixturesBasePath, relativePath); if (!fs.existsSync(absolutePath)) { throw new ErrorWithStack( `Fixture file ${relativePath} does not exist. diff --git a/spec/frontend/helpers/vue_component_helper.js b/spec/frontend/helpers/text_helper.js index e0fe18e5560..e0fe18e5560 100644 --- a/spec/frontend/helpers/vue_component_helper.js +++ b/spec/frontend/helpers/text_helper.js diff --git a/spec/frontend/helpers/timeout.js b/spec/frontend/helpers/timeout.js index 318593a48a4..b30b7f1ce1e 100644 --- a/spec/frontend/helpers/timeout.js +++ b/spec/frontend/helpers/timeout.js @@ -1,24 +1,31 @@ -let testTimeoutInMs; +const NS_PER_SEC = 1e9; +const NS_PER_MS = 1e6; -export const setTestTimeout = newTimeoutInMs => { - testTimeoutInMs = newTimeoutInMs; - jest.setTimeout(newTimeoutInMs); +let testTimeoutNS; + +export const setTestTimeout = newTimeoutMS => { + testTimeoutNS = newTimeoutMS * NS_PER_MS; + jest.setTimeout(newTimeoutMS); }; -export const initializeTestTimeout = defaultTimeoutInMs => { - setTestTimeout(defaultTimeoutInMs); +export const initializeTestTimeout = defaultTimeoutMS => { + setTestTimeout(defaultTimeoutMS); let testStartTime; // https://github.com/facebook/jest/issues/6947 beforeEach(() => { - testStartTime = Date.now(); + testStartTime = process.hrtime(); }); afterEach(() => { - const elapsedTimeInMs = Date.now() - testStartTime; - if (elapsedTimeInMs > testTimeoutInMs) { - throw new Error(`Test took too long (${elapsedTimeInMs}ms > ${testTimeoutInMs}ms)!`); + const [seconds, remainingNs] = process.hrtime(testStartTime); + const elapsedNS = seconds * NS_PER_SEC + remainingNs; + + if (elapsedNS > testTimeoutNS) { + throw new Error( + `Test took too long (${elapsedNS / NS_PER_MS}ms > ${testTimeoutNS / NS_PER_MS}ms)!`, + ); } }); }; diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js index 0878c1de095..9e920d59093 100644 --- a/spec/frontend/lib/utils/text_utility_spec.js +++ b/spec/frontend/lib/utils/text_utility_spec.js @@ -155,11 +155,11 @@ describe('text_utility', () => { expect(textUtils.truncateNamespace('a / b')).toBe('a'); }); - it(`should return the first 2 namespaces if the namespace inlcudes exactly 2 levels`, () => { + it(`should return the first 2 namespaces if the namespace includes exactly 2 levels`, () => { expect(textUtils.truncateNamespace('a / b / c')).toBe('a / b'); }); - it(`should return the first and last namespaces, separated by "...", if the namespace inlcudes more than 2 levels`, () => { + it(`should return the first and last namespaces, separated by "...", if the namespace includes more than 2 levels`, () => { expect(textUtils.truncateNamespace('a / b / c / d')).toBe('a / ... / c'); expect(textUtils.truncateNamespace('a / b / c / d / e / f / g / h / i')).toBe('a / ... / h'); }); diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js index 392c1b6533e..c3204b3aaa0 100644 --- a/spec/frontend/notes/components/discussion_notes_spec.js +++ b/spec/frontend/notes/components/discussion_notes_spec.js @@ -1,4 +1,4 @@ -import { mount, createLocalVue } from '@vue/test-utils'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; import '~/behaviors/markdown/render_gfm'; import { SYSTEM_NOTE } from '~/notes/constants'; import DiscussionNotes from '~/notes/components/discussion_notes.vue'; @@ -24,7 +24,7 @@ describe('DiscussionNotes', () => { store.dispatch('setNoteableData', noteableDataMock); store.dispatch('setNotesData', notesDataMock); - wrapper = mount(DiscussionNotes, { + wrapper = shallowMount(DiscussionNotes, { localVue, store, propsData: { diff --git a/spec/frontend/notes/stores/utils_spec.js b/spec/frontend/notes/stores/utils_spec.js new file mode 100644 index 00000000000..b31b7491334 --- /dev/null +++ b/spec/frontend/notes/stores/utils_spec.js @@ -0,0 +1,17 @@ +import { hasQuickActions } from '~/notes/stores/utils'; + +describe('hasQuickActions', () => { + it.each` + input | expected + ${'some comment'} | ${false} + ${'/quickaction'} | ${true} + ${'some comment with\n/quickaction'} | ${true} + `('returns $expected for $input', ({ input, expected }) => { + expect(hasQuickActions(input)).toBe(expected); + }); + + it('is stateless', () => { + expect(hasQuickActions('some comment')).toBe(hasQuickActions('some comment')); + expect(hasQuickActions('/quickaction')).toBe(hasQuickActions('/quickaction')); + }); +}); diff --git a/spec/frontend/operation_settings/components/external_dashboard_spec.js b/spec/frontend/operation_settings/components/external_dashboard_spec.js new file mode 100644 index 00000000000..de1dd219fe0 --- /dev/null +++ b/spec/frontend/operation_settings/components/external_dashboard_spec.js @@ -0,0 +1,100 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlButton, GlLink, GlFormGroup, GlFormInput } from '@gitlab/ui'; +import ExternalDashboard from '~/operation_settings/components/external_dashboard.vue'; +import { TEST_HOST } from 'helpers/test_constants'; + +describe('operation settings external dashboard component', () => { + let wrapper; + const externalDashboardPath = `http://mock-external-domain.com/external/dashboard/path`; + const externalDashboardHelpPagePath = `${TEST_HOST}/help/page/path`; + + beforeEach(() => { + wrapper = shallowMount(ExternalDashboard, { + propsData: { + externalDashboardPath, + externalDashboardHelpPagePath, + }, + }); + }); + + it('renders header text', () => { + expect(wrapper.find('.js-section-header').text()).toBe('External Dashboard'); + }); + + describe('sub-header', () => { + let subHeader; + + beforeEach(() => { + subHeader = wrapper.find('.js-section-sub-header'); + }); + + it('renders descriptive text', () => { + expect(subHeader.text()).toContain( + 'Add a button to the metrics dashboard linking directly to your existing external dashboards.', + ); + }); + + it('renders help page link', () => { + const link = subHeader.find(GlLink); + + expect(link.text()).toBe('Learn more'); + expect(link.attributes().href).toBe(externalDashboardHelpPagePath); + }); + }); + + describe('form', () => { + let form; + + beforeEach(() => { + form = wrapper.find('form'); + }); + + describe('external dashboard url', () => { + describe('input label', () => { + let formGroup; + + beforeEach(() => { + formGroup = form.find(GlFormGroup); + }); + + it('uses label text', () => { + expect(formGroup.attributes().label).toBe('Full dashboard URL'); + }); + + it('uses description text', () => { + expect(formGroup.attributes().description).toBe( + 'Enter the URL of the dashboard you want to link to', + ); + }); + }); + + describe('input field', () => { + let input; + + beforeEach(() => { + input = form.find(GlFormInput); + }); + + it('defaults to externalDashboardPath prop', () => { + expect(input.attributes().value).toBe(externalDashboardPath); + }); + + it('uses a placeholder', () => { + expect(input.attributes().placeholder).toBe('https://my-org.gitlab.io/my-dashboards'); + }); + }); + + describe('submit button', () => { + let submit; + + beforeEach(() => { + submit = form.find(GlButton); + }); + + it('renders button label', () => { + expect(submit.text()).toBe('Save Changes'); + }); + }); + }); + }); +}); diff --git a/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js b/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js index f163bdd9913..7e9aec84016 100644 --- a/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js +++ b/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js @@ -1,9 +1,6 @@ import $ from 'jquery'; import '~/lib/utils/text_utility'; import AbuseReports from '~/pages/admin/abuse_reports/abuse_reports'; -import { setTestTimeout } from 'helpers/timeout'; - -setTestTimeout(500); describe('Abuse Reports', () => { const FIXTURE = 'abuse_reports/abuse_reports_list.html'; diff --git a/spec/frontend/reports/components/report_item_spec.js b/spec/frontend/reports/components/report_item_spec.js new file mode 100644 index 00000000000..bacbb399513 --- /dev/null +++ b/spec/frontend/reports/components/report_item_spec.js @@ -0,0 +1,33 @@ +import { shallowMount } from '@vue/test-utils'; +import { STATUS_SUCCESS } from '~/reports/constants'; +import ReportItem from '~/reports/components/report_item.vue'; +import { componentNames } from '~/reports/components/issue_body'; + +describe('ReportItem', () => { + describe('showReportSectionStatusIcon', () => { + it('does not render CI Status Icon when showReportSectionStatusIcon is false', () => { + const wrapper = shallowMount(ReportItem, { + propsData: { + issue: { foo: 'bar' }, + component: componentNames.TestIssueBody, + status: STATUS_SUCCESS, + showReportSectionStatusIcon: false, + }, + }); + + expect(wrapper.find('issuestatusicon-stub').exists()).toBe(false); + }); + + it('shows status icon when unspecified', () => { + const wrapper = shallowMount(ReportItem, { + propsData: { + issue: { foo: 'bar' }, + component: componentNames.TestIssueBody, + status: STATUS_SUCCESS, + }, + }); + + expect(wrapper.find('issuestatusicon-stub').exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/repository/router_spec.js b/spec/frontend/repository/router_spec.js new file mode 100644 index 00000000000..f61a0ccd1e6 --- /dev/null +++ b/spec/frontend/repository/router_spec.js @@ -0,0 +1,23 @@ +import IndexPage from '~/repository/pages/index.vue'; +import TreePage from '~/repository/pages/tree.vue'; +import createRouter from '~/repository/router'; + +describe('Repository router spec', () => { + it.each` + path | component | componentName + ${'/'} | ${IndexPage} | ${'IndexPage'} + ${'/tree/master'} | ${TreePage} | ${'TreePage'} + ${'/tree/master/app/assets'} | ${TreePage} | ${'TreePage'} + ${'/tree/123/app/assets'} | ${null} | ${'null'} + `('sets component as $componentName for path "$path"', ({ path, component }) => { + const router = createRouter('', 'master'); + + const componentsForRoute = router.getMatchedComponents(path); + + expect(componentsForRoute.length).toBe(component ? 1 : 0); + + if (component) { + expect(componentsForRoute).toContain(component); + } + }); +}); diff --git a/spec/frontend/serverless/components/function_row_spec.js b/spec/frontend/serverless/components/function_row_spec.js index 414fdc5cd82..979f98c4832 100644 --- a/spec/frontend/serverless/components/function_row_spec.js +++ b/spec/frontend/serverless/components/function_row_spec.js @@ -1,27 +1,32 @@ import functionRowComponent from '~/serverless/components/function_row.vue'; import { shallowMount } from '@vue/test-utils'; +import Timeago from '~/vue_shared/components/time_ago_tooltip.vue'; import { mockServerlessFunction } from '../mock_data'; -const createComponent = func => - shallowMount(functionRowComponent, { propsData: { func }, sync: false }).vm; - describe('functionRowComponent', () => { - it('Parses the function details correctly', () => { - const vm = createComponent(mockServerlessFunction); + let wrapper; - expect(vm.$el.querySelector('b').innerHTML).toEqual(mockServerlessFunction.name); - expect(vm.$el.querySelector('span').innerHTML).toEqual(mockServerlessFunction.image); - expect(vm.$el.querySelector('timeago-stub').getAttribute('time')).not.toBe(null); + const createComponent = func => { + wrapper = shallowMount(functionRowComponent, { propsData: { func }, sync: false }); + }; - vm.$destroy(); + afterEach(() => { + wrapper.destroy(); + }); + + it('Parses the function details correctly', () => { + createComponent(mockServerlessFunction); + + expect(wrapper.find('b').text()).toBe(mockServerlessFunction.name); + expect(wrapper.find('span').text()).toBe(mockServerlessFunction.image); + expect(wrapper.find(Timeago).attributes('time')).not.toBe(null); }); it('handles clicks correctly', () => { - const vm = createComponent(mockServerlessFunction); + createComponent(mockServerlessFunction); + const { vm } = wrapper; expect(vm.checkClass(vm.$el.querySelector('p'))).toBe(true); // check somewhere inside the row - - vm.$destroy(); }); }); diff --git a/spec/frontend/serverless/components/functions_spec.js b/spec/frontend/serverless/components/functions_spec.js index 7af33ceaadc..6924fb9e91f 100644 --- a/spec/frontend/serverless/components/functions_spec.js +++ b/spec/frontend/serverless/components/functions_spec.js @@ -1,9 +1,12 @@ import Vuex from 'vuex'; +import { GlLoadingIcon } from '@gitlab/ui'; import AxiosMockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import functionsComponent from '~/serverless/components/functions.vue'; import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createStore } from '~/serverless/store'; +import EmptyState from '~/serverless/components/empty_state.vue'; +import EnvironmentRow from '~/serverless/components/environment_row.vue'; import { TEST_HOST } from 'helpers/test_constants'; import { mockServerlessFunctions } from '../mock_data'; @@ -43,7 +46,7 @@ describe('functionsComponent', () => { sync: false, }); - expect(component.vm.$el.querySelector('emptystate-stub')).not.toBe(null); + expect(component.find(EmptyState).exists()).toBe(true); }); it('should render a loading component', () => { @@ -60,7 +63,7 @@ describe('functionsComponent', () => { sync: false, }); - expect(component.vm.$el.querySelector('glloadingicon-stub')).not.toBe(null); + expect(component.find(GlLoadingIcon).exists()).toBe(true); }); it('should render empty state when there is no function data', () => { @@ -104,7 +107,7 @@ describe('functionsComponent', () => { component.vm.$store.dispatch('receiveFunctionsSuccess', mockServerlessFunctions); return component.vm.$nextTick().then(() => { - expect(component.vm.$el.querySelector('environmentrow-stub')).not.toBe(null); + expect(component.find(EnvironmentRow).exists()).toBe(true); }); }); }); diff --git a/spec/frontend/serverless/components/missing_prometheus_spec.js b/spec/frontend/serverless/components/missing_prometheus_spec.js index d0df6125290..5dbdccde2de 100644 --- a/spec/frontend/serverless/components/missing_prometheus_spec.js +++ b/spec/frontend/serverless/components/missing_prometheus_spec.js @@ -1,3 +1,4 @@ +import { GlButton } from '@gitlab/ui'; import missingPrometheusComponent from '~/serverless/components/missing_prometheus.vue'; import { shallowMount } from '@vue/test-utils'; @@ -9,27 +10,29 @@ const createComponent = missingData => missingData, }, sync: false, - }).vm; + }); describe('missingPrometheusComponent', () => { - let vm; + let wrapper; afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); it('should render missing prometheus message', () => { - vm = createComponent(false); + wrapper = createComponent(false); + const { vm } = wrapper; expect(vm.$el.querySelector('.state-description').innerHTML.trim()).toContain( 'Function invocation metrics require Prometheus to be installed first.', ); - expect(vm.$el.querySelector('glbutton-stub').getAttribute('variant')).toEqual('success'); + expect(wrapper.find(GlButton).attributes('variant')).toBe('success'); }); it('should render no prometheus data message', () => { - vm = createComponent(true); + wrapper = createComponent(true); + const { vm } = wrapper; expect(vm.$el.querySelector('.state-description').innerHTML.trim()).toContain( 'Invocation metrics loading or not available at this time.', diff --git a/spec/frontend/serverless/components/url_spec.js b/spec/frontend/serverless/components/url_spec.js index d05a9bba103..706441e8a8b 100644 --- a/spec/frontend/serverless/components/url_spec.js +++ b/spec/frontend/serverless/components/url_spec.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import urlComponent from '~/serverless/components/url.vue'; import { shallowMount } from '@vue/test-utils'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; const createComponent = uri => shallowMount(Vue.extend(urlComponent), { @@ -8,15 +9,16 @@ const createComponent = uri => uri, }, sync: false, - }).vm; + }); describe('urlComponent', () => { it('should render correctly', () => { const uri = 'http://testfunc.apps.example.com'; - const vm = createComponent(uri); + const wrapper = createComponent(uri); + const { vm } = wrapper; expect(vm.$el.classList.contains('clipboard-group')).toBe(true); - expect(vm.$el.querySelector('clipboardbutton-stub').getAttribute('text')).toEqual(uri); + expect(wrapper.find(ClipboardButton).attributes('text')).toEqual(uri); expect(vm.$el.querySelector('.url-text-field').innerHTML).toEqual(uri); diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js index c57e0e7cfc6..c24f0bc4776 100644 --- a/spec/frontend/test_setup.js +++ b/spec/frontend/test_setup.js @@ -3,7 +3,7 @@ import * as jqueryMatchers from 'custom-jquery-matchers'; import Translate from '~/vue_shared/translate'; import axios from '~/lib/utils/axios_utils'; import { initializeTestTimeout } from './helpers/timeout'; -import { getJSONFixture, loadHTMLFixture, setHTMLFixture } from './helpers/fixtures'; +import { loadHTMLFixture, setHTMLFixture } from './helpers/fixtures'; process.on('unhandledRejection', global.promiseRejectionHandler); @@ -15,7 +15,7 @@ afterEach(() => }), ); -initializeTestTimeout(300); +initializeTestTimeout(500); // fail tests for unmocked requests beforeEach(done => { @@ -46,9 +46,12 @@ Object.defineProperty(global.Element.prototype, 'innerText', { // convenience wrapper for migration from Karma Object.assign(global, { loadFixtures: loadHTMLFixture, - loadJSONFixtures: getJSONFixture, - preloadFixtures() {}, setFixtures: setHTMLFixture, + + // The following functions fill the fixtures cache in Karma. + // This is not necessary in Jest because we make no Ajax request. + loadJSONFixtures() {}, + preloadFixtures() {}, }); // custom-jquery-matchers was written for an old Jest version, we need to make it compatible diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js new file mode 100644 index 00000000000..3b6f67457ad --- /dev/null +++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js @@ -0,0 +1,103 @@ +import { GlLoadingIcon } from '@gitlab/ui'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import SuggestionDiffHeader from '~/vue_shared/components/markdown/suggestion_diff_header.vue'; + +const localVue = createLocalVue(); + +const DEFAULT_PROPS = { + canApply: true, + isApplied: false, + helpPagePath: 'path_to_docs', +}; + +describe('Suggestion Diff component', () => { + let wrapper; + + const createComponent = props => { + wrapper = shallowMount(localVue.extend(SuggestionDiffHeader), { + propsData: { + ...DEFAULT_PROPS, + ...props, + }, + localVue, + sync: false, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findApplyButton = () => wrapper.find('.qa-apply-btn'); + const findHeader = () => wrapper.find('.qa-suggestion-diff-header'); + const findHelpButton = () => wrapper.find('.js-help-btn'); + const findLoading = () => wrapper.find(GlLoadingIcon); + + it('renders a suggestion header', () => { + createComponent(); + + const header = findHeader(); + + expect(header.exists()).toBe(true); + expect(header.html().includes('Suggested change')).toBe(true); + }); + + it('renders a help button', () => { + createComponent(); + + expect(findHelpButton().exists()).toBe(true); + }); + + it('renders an apply button', () => { + createComponent(); + + const applyBtn = findApplyButton(); + + expect(applyBtn.exists()).toBe(true); + expect(applyBtn.html().includes('Apply suggestion')).toBe(true); + }); + + it('does not render an apply button if `canApply` is set to false', () => { + createComponent({ canApply: false }); + + expect(findApplyButton().exists()).toBe(false); + }); + + describe('when apply suggestion is clicked', () => { + beforeEach(done => { + createComponent(); + + findApplyButton().vm.$emit('click'); + + wrapper.vm.$nextTick(done); + }); + + it('emits apply', () => { + expect(wrapper.emittedByOrder()).toEqual([{ name: 'apply', args: [expect.any(Function)] }]); + }); + + it('hides apply button', () => { + expect(findApplyButton().exists()).toBe(false); + }); + + it('shows loading', () => { + expect(findLoading().exists()).toBe(true); + expect(wrapper.text()).toContain('Applying suggestion'); + }); + + it('when callback of apply is called, hides loading', done => { + const [callback] = wrapper.emitted().apply[0]; + + callback(); + + wrapper.vm + .$nextTick() + .then(() => { + expect(findApplyButton().exists()).toBe(true); + expect(findLoading().exists()).toBe(false); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/graphql/gitlab_schema_spec.rb b/spec/graphql/gitlab_schema_spec.rb index 05f10fb40f0..c138c87c4ac 100644 --- a/spec/graphql/gitlab_schema_spec.rb +++ b/spec/graphql/gitlab_schema_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' describe GitlabSchema do + let(:user) { build :user } + it 'uses batch loading' do expect(field_instrumenters).to include(BatchLoader::GraphQL) end @@ -33,43 +35,75 @@ describe GitlabSchema do expect(connection).to eq(Gitlab::Graphql::Connections::KeysetConnection) end - context 'for different types of users' do - it 'returns DEFAULT_MAX_COMPLEXITY for no context' do - expect(GraphQL::Schema) - .to receive(:execute) - .with('query', hash_including(max_complexity: GitlabSchema::DEFAULT_MAX_COMPLEXITY)) + describe '.execute' do + context 'for different types of users' do + context 'when no context' do + it 'returns DEFAULT_MAX_COMPLEXITY' do + expect(GraphQL::Schema) + .to receive(:execute) + .with('query', hash_including(max_complexity: GitlabSchema::DEFAULT_MAX_COMPLEXITY)) - described_class.execute('query') - end + described_class.execute('query') + end + end - it 'returns DEFAULT_MAX_COMPLEXITY for no user' do - expect(GraphQL::Schema) - .to receive(:execute) - .with('query', hash_including(max_complexity: GitlabSchema::DEFAULT_MAX_COMPLEXITY)) + context 'when no user' do + it 'returns DEFAULT_MAX_COMPLEXITY' do + expect(GraphQL::Schema) + .to receive(:execute) + .with('query', hash_including(max_complexity: GitlabSchema::DEFAULT_MAX_COMPLEXITY)) - described_class.execute('query', context: {}) - end + described_class.execute('query', context: {}) + end - it 'returns AUTHENTICATED_COMPLEXITY for a logged in user' do - user = build :user + it 'returns ANONYMOUS_MAX_DEPTH' do + expect(GraphQL::Schema) + .to receive(:execute) + .with('query', hash_including(max_depth: GitlabSchema::ANONYMOUS_MAX_DEPTH)) - expect(GraphQL::Schema).to receive(:execute).with('query', hash_including(max_complexity: GitlabSchema::AUTHENTICATED_COMPLEXITY)) + described_class.execute('query', context: {}) + end + end - described_class.execute('query', context: { current_user: user }) - end + context 'when a logged in user' do + it 'returns AUTHENTICATED_COMPLEXITY' do + expect(GraphQL::Schema).to receive(:execute).with('query', hash_including(max_complexity: GitlabSchema::AUTHENTICATED_COMPLEXITY)) - it 'returns ADMIN_COMPLEXITY for an admin user' do - user = build :user, :admin + described_class.execute('query', context: { current_user: user }) + end - expect(GraphQL::Schema).to receive(:execute).with('query', hash_including(max_complexity: GitlabSchema::ADMIN_COMPLEXITY)) + it 'returns AUTHENTICATED_MAX_DEPTH' do + expect(GraphQL::Schema).to receive(:execute).with('query', hash_including(max_depth: GitlabSchema::AUTHENTICATED_MAX_DEPTH)) - described_class.execute('query', context: { current_user: user }) - end + described_class.execute('query', context: { current_user: user }) + end + end + + context 'when an admin user' do + it 'returns ADMIN_COMPLEXITY' do + user = build :user, :admin + + expect(GraphQL::Schema).to receive(:execute).with('query', hash_including(max_complexity: GitlabSchema::ADMIN_COMPLEXITY)) + + described_class.execute('query', context: { current_user: user }) + end + end + + context 'when max_complexity passed on the query' do + it 'returns what was passed on the query' do + expect(GraphQL::Schema).to receive(:execute).with('query', hash_including(max_complexity: 1234)) + + described_class.execute('query', max_complexity: 1234) + end + end - it 'returns what was passed on the query' do - expect(GraphQL::Schema).to receive(:execute).with('query', { max_complexity: 1234 }) + context 'when max_depth passed on the query' do + it 'returns what was passed on the query' do + expect(GraphQL::Schema).to receive(:execute).with('query', hash_including(max_depth: 1234)) - described_class.execute('query', max_complexity: 1234) + described_class.execute('query', max_depth: 1234) + end + end end end diff --git a/spec/helpers/dashboard_helper_spec.rb b/spec/helpers/dashboard_helper_spec.rb index 7ba24ba2956..023238ee0ae 100644 --- a/spec/helpers/dashboard_helper_spec.rb +++ b/spec/helpers/dashboard_helper_spec.rb @@ -21,4 +21,10 @@ describe DashboardHelper do expect(helper.dashboard_nav_links).not_to include(:activity, :milestones) end end + + describe '.has_start_trial?' do + subject { helper.has_start_trial? } + + it { is_expected.to eq(false) } + end end diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb index 03b4c19ec22..0434af25866 100644 --- a/spec/helpers/emails_helper_spec.rb +++ b/spec/helpers/emails_helper_spec.rb @@ -1,6 +1,45 @@ require 'spec_helper' describe EmailsHelper do + describe 'closure_reason_text' do + context 'when given a MergeRequest' do + let(:merge_request) { create(:merge_request) } + let(:merge_request_presenter) { merge_request.present } + + context "and format is text" do + it "returns plain text" do + expect(closure_reason_text(merge_request, format: :text)).to eq(" via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})") + end + end + + context "and format is HTML" do + it "returns HTML" do + expect(closure_reason_text(merge_request, format: :html)).to eq(" via merge request #{link_to(merge_request.to_reference, merge_request_presenter.web_url)}") + end + end + + context "and format is unknown" do + it "returns plain text" do + expect(closure_reason_text(merge_request, format: :text)).to eq(" via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})") + end + end + end + + context 'when given a String' do + let(:closed_via) { "5a0eb6fd7e0f133044378c662fcbbc0d0c16dbfa" } + + it "returns plain text" do + expect(closure_reason_text(closed_via)).to eq(" via #{closed_via}") + end + end + + context 'when not given anything' do + it "returns empty string" do + expect(closure_reason_text(nil)).to eq("") + end + end + end + describe 'sanitize_name' do context 'when name contains a valid URL string' do it 'returns name with `.` replaced with `_` to prevent mail clients from auto-linking URLs' do diff --git a/spec/helpers/nav_helper_spec.rb b/spec/helpers/nav_helper_spec.rb index e840c927d59..979d89812f5 100644 --- a/spec/helpers/nav_helper_spec.rb +++ b/spec/helpers/nav_helper_spec.rb @@ -50,4 +50,16 @@ describe NavHelper do expect(helper.header_links).to contain_exactly(:sign_in, :search) end end + + context '.admin_monitoring_nav_links' do + subject { helper.admin_monitoring_nav_links } + + it { is_expected.to all(be_a(String)) } + end + + context '.group_issues_sub_menu_items' do + subject { helper.group_issues_sub_menu_items } + + it { is_expected.to all(be_a(String)) } + end end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 37c63807c82..83271aa24a3 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -445,6 +445,10 @@ describe ProjectsHelper do Project.all end + before do + stub_feature_flags(project_list_filter_bar: false) + end + it 'returns true when there are projects' do expect(helper.show_projects?(projects, {})).to eq(true) end @@ -795,4 +799,24 @@ describe ProjectsHelper do it { is_expected.to eq(result) } end end + + describe '#can_import_members?' do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:owner) { project.owner } + + before do + helper.instance_variable_set(:@project, project) + end + + it 'returns false if user cannot admin_project_member' do + allow(helper).to receive(:current_user) { user } + expect(helper.can_import_members?).to eq false + end + + it 'returns true if user can admin_project_member' do + allow(helper).to receive(:current_user) { owner } + expect(helper.can_import_members?).to eq true + end + end end diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb index 726ce07a2d1..77bc28a6b07 100644 --- a/spec/initializers/secret_token_spec.rb +++ b/spec/initializers/secret_token_spec.rb @@ -45,11 +45,21 @@ describe 'create_tokens' do expect(keys).to all(match(RSA_KEY)) end + it "generates private key for Let's Encrypt" do + create_tokens + + keys = secrets.values_at(:lets_encrypt_private_key) + + expect(keys.uniq).to eq(keys) + expect(keys).to all(match(RSA_KEY)) + 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') expect(self).to receive(:warn_missing_secret).with('openid_connect_signing_key') + expect(self).to receive(:warn_missing_secret).with('lets_encrypt_private_key') create_tokens end @@ -78,6 +88,7 @@ describe 'create_tokens' do before do secrets.db_key_base = 'db_key_base' secrets.openid_connect_signing_key = 'openid_connect_signing_key' + secrets.lets_encrypt_private_key = 'lets_encrypt_private_key' allow(File).to receive(:exist?).with('.secret').and_return(true) allow(File).to receive(:read).with('.secret').and_return('file_key') diff --git a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js index 481b1a4d4b0..2839922fbd3 100644 --- a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js +++ b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js @@ -113,7 +113,7 @@ describe('AjaxFormVariableList', () => { it('hides secret values', done => { mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200, {}); - const row = container.querySelector('.js-row:first-child'); + const row = container.querySelector('.js-row'); const valueInput = row.querySelector('.js-ci-variable-input-value'); const valuePlaceholder = row.querySelector('.js-secret-value-placeholder'); diff --git a/spec/javascripts/diffs/components/commit_item_spec.js b/spec/javascripts/diffs/components/commit_item_spec.js index 50e45f48af3..8fc9b10dd0b 100644 --- a/spec/javascripts/diffs/components/commit_item_spec.js +++ b/spec/javascripts/diffs/components/commit_item_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import { TEST_HOST } from 'spec/test_constants'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import { trimText } from 'spec/helpers/vue_component_helper'; +import { trimText } from 'spec/helpers/text_helper'; import { getTimeago } from '~/lib/utils/datetime_utility'; import CommitItem from '~/diffs/components/commit_item.vue'; import getDiffWithCommit from '../mock_data/diff_with_commit'; diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js index c82dcadd2f1..6309a8823d7 100644 --- a/spec/javascripts/diffs/store/actions_spec.js +++ b/spec/javascripts/diffs/store/actions_spec.js @@ -82,7 +82,7 @@ describe('DiffsStoreActions', () => { describe('fetchDiffFiles', () => { it('should fetch diff files', done => { - const endpoint = '/fetch/diff/files'; + const endpoint = '/fetch/diff/files?w=1'; const mock = new MockAdapter(axios); const res = { diff_files: 1, merge_request_diffs: [] }; mock.onGet(endpoint).reply(200, res); @@ -828,6 +828,10 @@ describe('DiffsStoreActions', () => { }); describe('setShowWhitespace', () => { + beforeEach(() => { + spyOn(eventHub, '$emit').and.stub(); + }); + it('commits SET_SHOW_WHITESPACE', done => { testAction( setShowWhitespace, @@ -855,6 +859,30 @@ describe('DiffsStoreActions', () => { expect(window.history.pushState).toHaveBeenCalled(); }); + + it('calls history pushState with merged params', () => { + const originalPushState = window.history; + + originalPushState.pushState({}, '', '?test=1'); + + spyOn(localStorage, 'setItem').and.stub(); + spyOn(window.history, 'pushState').and.stub(); + + setShowWhitespace({ commit() {} }, { showWhitespace: true, pushState: true }); + + expect(window.history.pushState.calls.mostRecent().args[2]).toMatch(/(.*)\?test=1&w=0/); + + originalPushState.pushState({}, '', '?'); + }); + + it('emits eventHub event', () => { + spyOn(localStorage, 'setItem').and.stub(); + spyOn(window.history, 'pushState').and.stub(); + + setShowWhitespace({ commit() {} }, { showWhitespace: true, pushState: true }); + + expect(eventHub.$emit).toHaveBeenCalledWith('refetchDiffData'); + }); }); describe('setRenderIt', () => { diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js index ff15067aeac..f1c323df4be 100644 --- a/spec/javascripts/environments/folder/environments_folder_view_spec.js +++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js @@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import environmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import { removeBreakLine, removeWhitespace } from 'spec/helpers/vue_component_helper'; +import { removeBreakLine, removeWhitespace } from 'spec/helpers/text_helper'; import { environmentsList } from '../mock_data'; describe('Environments Folder View', () => { diff --git a/spec/javascripts/fixtures/.gitignore b/spec/javascripts/fixtures/.gitignore index 2507c8e7263..bed020f5b0a 100644 --- a/spec/javascripts/fixtures/.gitignore +++ b/spec/javascripts/fixtures/.gitignore @@ -1,3 +1,5 @@ *.html.raw *.html *.json +*.pdf +*.bmpr diff --git a/spec/javascripts/fixtures/abuse_reports.rb b/spec/javascripts/fixtures/abuse_reports.rb index 54b6419bcdb..e0aaecf626a 100644 --- a/spec/javascripts/fixtures/abuse_reports.rb +++ b/spec/javascripts/fixtures/abuse_reports.rb @@ -18,10 +18,9 @@ describe Admin::AbuseReportsController, '(JavaScript fixtures)', type: :controll sign_in(admin) end - it 'abuse_reports/abuse_reports_list.html' do |example| + it 'abuse_reports/abuse_reports_list.html' do get :index expect(response).to be_success - store_frontend_fixture(response, example.description) end end diff --git a/spec/javascripts/fixtures/admin_users.rb b/spec/javascripts/fixtures/admin_users.rb index 76dbdf603da..22a5de66577 100644 --- a/spec/javascripts/fixtures/admin_users.rb +++ b/spec/javascripts/fixtures/admin_users.rb @@ -17,13 +17,12 @@ describe Admin::UsersController, '(JavaScript fixtures)', type: :controller do clean_frontend_fixtures('admin/users') end - it 'admin/users/new_with_internal_user_regex.html' do |example| + it 'admin/users/new_with_internal_user_regex.html' do stub_application_setting(user_default_external: true) stub_application_setting(user_default_internal_regex: '^(?:(?!\.ext@).)*$\r?') get :new expect(response).to be_success - store_frontend_fixture(response, example.description) end end diff --git a/spec/javascripts/fixtures/application_settings.rb b/spec/javascripts/fixtures/application_settings.rb index c535e598e12..d4651fa6ece 100644 --- a/spec/javascripts/fixtures/application_settings.rb +++ b/spec/javascripts/fixtures/application_settings.rb @@ -23,12 +23,11 @@ describe Admin::ApplicationSettingsController, '(JavaScript fixtures)', type: :c remove_repository(project) end - it 'application_settings/accounts_and_limit.html' do |example| + it 'application_settings/accounts_and_limit.html' do stub_application_setting(user_default_external: false) get :show expect(response).to be_success - store_frontend_fixture(response, example.description) end end diff --git a/spec/javascripts/fixtures/autocomplete_sources.rb b/spec/javascripts/fixtures/autocomplete_sources.rb index c117fb7cd24..b20a0159d7d 100644 --- a/spec/javascripts/fixtures/autocomplete_sources.rb +++ b/spec/javascripts/fixtures/autocomplete_sources.rb @@ -18,7 +18,7 @@ describe Projects::AutocompleteSourcesController, '(JavaScript fixtures)', type: sign_in(admin) end - it 'autocomplete_sources/labels.json' do |example| + it 'autocomplete_sources/labels.json' do issue.labels << create(:label, project: project, title: 'bug') issue.labels << create(:label, project: project, title: 'critical') @@ -35,6 +35,5 @@ describe Projects::AutocompleteSourcesController, '(JavaScript fixtures)', type: } expect(response).to be_success - store_frontend_fixture(response, example.description) end end diff --git a/spec/javascripts/fixtures/balsamiq.rb b/spec/javascripts/fixtures/balsamiq.rb deleted file mode 100644 index 234e246119a..00000000000 --- a/spec/javascripts/fixtures/balsamiq.rb +++ /dev/null @@ -1,18 +0,0 @@ -require 'spec_helper' - -describe 'Balsamiq file', '(JavaScript fixtures)', type: :controller do - include JavaScriptFixturesHelpers - - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} - let(:project) { create(:project, :repository, namespace: namespace, path: 'balsamiq-project') } - - before(:all) do - clean_frontend_fixtures('blob/balsamiq/') - end - - it 'blob/balsamiq/test.bmpr' do |example| - blob = project.repository.blob_at('b89b56d79', 'files/images/balsamiq.bmpr') - - store_frontend_fixture(blob.data.force_encoding('utf-8'), example.description) - end -end diff --git a/spec/javascripts/fixtures/blob.rb b/spec/javascripts/fixtures/blob.rb index db7749bc000..07670552cd5 100644 --- a/spec/javascripts/fixtures/blob.rb +++ b/spec/javascripts/fixtures/blob.rb @@ -22,7 +22,7 @@ describe Projects::BlobController, '(JavaScript fixtures)', type: :controller do remove_repository(project) end - it 'blob/show.html' do |example| + it 'blob/show.html' do get(:show, params: { namespace_id: project.namespace, project_id: project, @@ -30,6 +30,5 @@ describe Projects::BlobController, '(JavaScript fixtures)', type: :controller do }) expect(response).to be_success - store_frontend_fixture(response, example.description) end end diff --git a/spec/javascripts/fixtures/boards.rb b/spec/javascripts/fixtures/boards.rb index c4390e89578..5835721d3d5 100644 --- a/spec/javascripts/fixtures/boards.rb +++ b/spec/javascripts/fixtures/boards.rb @@ -17,13 +17,12 @@ describe Projects::BoardsController, '(JavaScript fixtures)', type: :controller sign_in(admin) end - it 'boards/show.html' do |example| + it 'boards/show.html' do get(:index, params: { namespace_id: project.namespace, project_id: project }) expect(response).to be_success - store_frontend_fixture(response, example.description) end end diff --git a/spec/javascripts/fixtures/branches.rb b/spec/javascripts/fixtures/branches.rb index 5d2d6c7ec0e..204aa9b7c7a 100644 --- a/spec/javascripts/fixtures/branches.rb +++ b/spec/javascripts/fixtures/branches.rb @@ -21,13 +21,12 @@ describe Projects::BranchesController, '(JavaScript fixtures)', type: :controlle remove_repository(project) end - it 'branches/new_branch.html' do |example| + it 'branches/new_branch.html' do get :new, params: { namespace_id: project.namespace.to_param, project_id: project } expect(response).to be_success - store_frontend_fixture(response, example.description) end end diff --git a/spec/javascripts/fixtures/clusters.rb b/spec/javascripts/fixtures/clusters.rb index 8ebd8a41366..1076404e0e3 100644 --- a/spec/javascripts/fixtures/clusters.rb +++ b/spec/javascripts/fixtures/clusters.rb @@ -22,7 +22,7 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle remove_repository(project) end - it 'clusters/show_cluster.html' do |example| + it 'clusters/show_cluster.html' do get :show, params: { namespace_id: project.namespace.to_param, project_id: project, @@ -30,6 +30,5 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle } expect(response).to be_success - store_frontend_fixture(response, example.description) end end diff --git a/spec/javascripts/fixtures/commit.rb b/spec/javascripts/fixtures/commit.rb index ab10f559e4b..ff9a4bc1adc 100644 --- a/spec/javascripts/fixtures/commit.rb +++ b/spec/javascripts/fixtures/commit.rb @@ -19,7 +19,7 @@ describe Projects::CommitController, '(JavaScript fixtures)', type: :controller allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon') end - it 'commit/show.html' do |example| + it 'commit/show.html' do params = { namespace_id: project.namespace, project_id: project, @@ -29,6 +29,5 @@ describe Projects::CommitController, '(JavaScript fixtures)', type: :controller get :show, params: params expect(response).to be_success - store_frontend_fixture(response, example.description) end end diff --git a/spec/javascripts/fixtures/deploy_keys.rb b/spec/javascripts/fixtures/deploy_keys.rb index a333d9c0150..38eab853da2 100644 --- a/spec/javascripts/fixtures/deploy_keys.rb +++ b/spec/javascripts/fixtures/deploy_keys.rb @@ -24,7 +24,7 @@ describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :control render_views - it 'deploy_keys/keys.json' do |example| + it 'deploy_keys/keys.json' do create(:rsa_deploy_key_2048, public: true) project_key = create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCdMHEHyhRjbhEZVddFn6lTWdgEy5Q6Bz4nwGB76xWZI5YT/1WJOMEW+sL5zYd31kk7sd3FJ5L9ft8zWMWrr/iWXQikC2cqZK24H1xy+ZUmrRuJD4qGAaIVoyyzBL+avL+lF8J5lg6YSw8gwJY/lX64/vnJHUlWw2n5BF8IFOWhiw== dummy@gitlab.com') internal_key = create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDNd/UJWhPrpb+b/G5oL109y57yKuCxE+WUGJGYaj7WQKsYRJmLYh1mgjrl+KVyfsWpq4ylOxIfFSnN9xBBFN8mlb0Fma5DC7YsSsibJr3MZ19ZNBprwNcdogET7aW9I0In7Wu5f2KqI6e5W/spJHCy4JVxzVMUvk6Myab0LnJ2iQ== dummy@gitlab.com') @@ -39,6 +39,5 @@ describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :control }, format: :json expect(response).to be_success - store_frontend_fixture(response, example.description) end end diff --git a/spec/javascripts/fixtures/groups.rb b/spec/javascripts/fixtures/groups.rb index 16e31028b05..4d0afc3ce1a 100644 --- a/spec/javascripts/fixtures/groups.rb +++ b/spec/javascripts/fixtures/groups.rb @@ -18,20 +18,18 @@ describe 'Groups (JavaScript fixtures)', type: :controller do end describe GroupsController, '(JavaScript fixtures)', type: :controller do - it 'groups/edit.html' do |example| + it 'groups/edit.html' do get :edit, params: { id: group } expect(response).to be_success - store_frontend_fixture(response, example.description) end end describe Groups::Settings::CiCdController, '(JavaScript fixtures)', type: :controller do - it 'groups/ci_cd_settings.html' do |example| + it 'groups/ci_cd_settings.html' do get :show, params: { group_id: group } expect(response).to be_success - store_frontend_fixture(response, example.description) end end end diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb index 0f3f9a10f94..d8d77f767de 100644 --- a/spec/javascripts/fixtures/issues.rb +++ b/spec/javascripts/fixtures/issues.rb @@ -21,26 +21,26 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller remove_repository(project) end - it 'issues/open-issue.html' do |example| - render_issue(example.description, create(:issue, project: project)) + it 'issues/open-issue.html' do + render_issue(create(:issue, project: project)) end - it 'issues/closed-issue.html' do |example| - render_issue(example.description, create(:closed_issue, project: project)) + it 'issues/closed-issue.html' do + render_issue(create(:closed_issue, project: project)) end - it 'issues/issue-with-task-list.html' do |example| + it 'issues/issue-with-task-list.html' do issue = create(:issue, project: project, description: '- [ ] Task List Item') - render_issue(example.description, issue) + render_issue(issue) end - it 'issues/issue_with_comment.html' do |example| + it 'issues/issue_with_comment.html' do issue = create(:issue, project: project) create(:note, project: project, noteable: issue, note: '- [ ] Task List Item').save - render_issue(example.description, issue) + render_issue(issue) end - it 'issues/issue_list.html' do |example| + it 'issues/issue_list.html' do create(:issue, project: project) get :index, params: { @@ -49,12 +49,11 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller } expect(response).to be_success - store_frontend_fixture(response, example.description) end private - def render_issue(fixture_file_name, issue) + def render_issue(issue) get :show, params: { namespace_id: project.namespace.to_param, project_id: project, @@ -62,7 +61,6 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller } expect(response).to be_success - store_frontend_fixture(response, fixture_file_name) end end @@ -89,7 +87,7 @@ describe API::Issues, '(JavaScript fixtures)', type: :request do end end - it 'issues/related_merge_requests.json' do |example| + it 'issues/related_merge_requests.json' do user = create(:user) project = create(:project, :public, creator_id: user.id, namespace: user.namespace) issue_title = 'foo' @@ -120,6 +118,5 @@ describe API::Issues, '(JavaScript fixtures)', type: :request do get_related_merge_requests(project.id, issue.iid, user) expect(response).to be_success - store_frontend_fixture(response, example.description) end end diff --git a/spec/javascripts/fixtures/jobs.rb b/spec/javascripts/fixtures/jobs.rb index 941235190b5..46ccd6f8c8a 100644 --- a/spec/javascripts/fixtures/jobs.rb +++ b/spec/javascripts/fixtures/jobs.rb @@ -32,7 +32,7 @@ describe Projects::JobsController, '(JavaScript fixtures)', type: :controller do remove_repository(project) end - it 'builds/build-with-artifacts.html' do |example| + it 'builds/build-with-artifacts.html' do get :show, params: { namespace_id: project.namespace.to_param, project_id: project, @@ -40,10 +40,9 @@ describe Projects::JobsController, '(JavaScript fixtures)', type: :controller do } expect(response).to be_success - store_frontend_fixture(response, example.description) end - it 'jobs/delayed.json' do |example| + it 'jobs/delayed.json' do get :show, params: { namespace_id: project.namespace.to_param, project_id: project, @@ -51,6 +50,5 @@ describe Projects::JobsController, '(JavaScript fixtures)', type: :controller do }, format: :json expect(response).to be_success - store_frontend_fixture(response, example.description) end end diff --git a/spec/javascripts/fixtures/labels.rb b/spec/javascripts/fixtures/labels.rb index 9420194e675..4d1b7317274 100644 --- a/spec/javascripts/fixtures/labels.rb +++ b/spec/javascripts/fixtures/labels.rb @@ -30,13 +30,12 @@ describe 'Labels (JavaScript fixtures)' do sign_in(admin) end - it 'labels/group_labels.json' do |example| + it 'labels/group_labels.json' do get :index, params: { group_id: group }, format: 'json' expect(response).to be_success - store_frontend_fixture(response, example.description) end end @@ -47,14 +46,13 @@ describe 'Labels (JavaScript fixtures)' do sign_in(admin) end - it 'labels/project_labels.json' do |example| + it 'labels/project_labels.json' do get :index, params: { namespace_id: group, project_id: project }, format: 'json' expect(response).to be_success - store_frontend_fixture(response, example.description) end end end diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb index 7df1e5cb512..05860be2291 100644 --- a/spec/javascripts/fixtures/merge_requests.rb +++ b/spec/javascripts/fixtures/merge_requests.rb @@ -42,52 +42,52 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont remove_repository(project) end - it 'merge_requests/merge_request_of_current_user.html' do |example| + it 'merge_requests/merge_request_of_current_user.html' do merge_request.update(author: admin) - render_merge_request(example.description, merge_request) + render_merge_request(merge_request) end - it 'merge_requests/merge_request_with_task_list.html' do |example| + it 'merge_requests/merge_request_with_task_list.html' do create(:ci_build, :pending, pipeline: pipeline) - render_merge_request(example.description, merge_request) + render_merge_request(merge_request) end - it 'merge_requests/merged_merge_request.html' do |example| + it 'merge_requests/merged_merge_request.html' do expect_next_instance_of(MergeRequest) do |merge_request| allow(merge_request).to receive(:source_branch_exists?).and_return(true) allow(merge_request).to receive(:can_remove_source_branch?).and_return(true) end - render_merge_request(example.description, merged_merge_request) + render_merge_request(merged_merge_request) end - it 'merge_requests/diff_comment.html' do |example| + it 'merge_requests/diff_comment.html' do create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request) create(:note_on_merge_request, author: admin, project: project, noteable: merge_request) - render_merge_request(example.description, merge_request) + render_merge_request(merge_request) end - it 'merge_requests/merge_request_with_comment.html' do |example| + it 'merge_requests/merge_request_with_comment.html' do create(:note_on_merge_request, author: admin, project: project, noteable: merge_request, note: '- [ ] Task List Item') - render_merge_request(example.description, merge_request) + render_merge_request(merge_request) end - it 'merge_requests/discussions.json' do |example| + it 'merge_requests/discussions.json' do create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request) - render_discussions_json(merge_request, example.description) + render_discussions_json(merge_request) end - it 'merge_requests/diff_discussion.json' do |example| + it 'merge_requests/diff_discussion.json' do create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request) - render_discussions_json(merge_request, example.description) + render_discussions_json(merge_request) end - it 'merge_requests/resolved_diff_discussion.json' do |example| + it 'merge_requests/resolved_diff_discussion.json' do note = create(:discussion_note_on_merge_request, :resolved, project: project, author: admin, position: position, noteable: merge_request) create(:system_note, project: project, author: admin, noteable: merge_request, discussion_id: note.discussion.id) - render_discussions_json(merge_request, example.description) + render_discussions_json(merge_request) end context 'with image diff' do @@ -106,25 +106,23 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont ) end - it 'merge_requests/image_diff_discussion.json' do |example| + it 'merge_requests/image_diff_discussion.json' do create(:diff_note_on_merge_request, project: project, noteable: merge_request2, position: image_position) - render_discussions_json(merge_request2, example.description) + render_discussions_json(merge_request2) end end private - def render_discussions_json(merge_request, fixture_file_name) + def render_discussions_json(merge_request) get :discussions, params: { namespace_id: project.namespace.to_param, project_id: project, id: merge_request.to_param }, format: :json - - store_frontend_fixture(response, fixture_file_name) end - def render_merge_request(fixture_file_name, merge_request) + def render_merge_request(merge_request) get :show, params: { namespace_id: project.namespace.to_param, project_id: project, @@ -132,6 +130,5 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont }, format: :html expect(response).to be_success - store_frontend_fixture(response, fixture_file_name) end end diff --git a/spec/javascripts/fixtures/merge_requests_diffs.rb b/spec/javascripts/fixtures/merge_requests_diffs.rb index 57462e74bb2..03b9b713fd8 100644 --- a/spec/javascripts/fixtures/merge_requests_diffs.rb +++ b/spec/javascripts/fixtures/merge_requests_diffs.rb @@ -34,29 +34,29 @@ describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)', type remove_repository(project) end - it 'merge_request_diffs/with_commit.json' do |example| + it 'merge_request_diffs/with_commit.json' do # Create a user that matches the selected commit author # This is so that the "author" information will be populated create(:user, email: selected_commit.author_email, name: selected_commit.author_name) - render_merge_request(example.description, merge_request, commit_id: selected_commit.sha) + render_merge_request(merge_request, commit_id: selected_commit.sha) end - it 'merge_request_diffs/inline_changes_tab_with_comments.json' do |example| + it 'merge_request_diffs/inline_changes_tab_with_comments.json' do create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request) create(:note_on_merge_request, author: admin, project: project, noteable: merge_request) - render_merge_request(example.description, merge_request) + render_merge_request(merge_request) end - it 'merge_request_diffs/parallel_changes_tab_with_comments.json' do |example| + it 'merge_request_diffs/parallel_changes_tab_with_comments.json' do create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request) create(:note_on_merge_request, author: admin, project: project, noteable: merge_request) - render_merge_request(example.description, merge_request, view: 'parallel') + render_merge_request(merge_request, view: 'parallel') end private - def render_merge_request(fixture_file_name, merge_request, view: 'inline', **extra_params) + def render_merge_request(merge_request, view: 'inline', **extra_params) get :show, params: { namespace_id: project.namespace.to_param, project_id: project, @@ -66,6 +66,5 @@ describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)', type }, format: :json expect(response).to be_success - store_frontend_fixture(response, fixture_file_name) end end diff --git a/spec/javascripts/fixtures/pdf.rb b/spec/javascripts/fixtures/pdf.rb deleted file mode 100644 index ef9976b9fd3..00000000000 --- a/spec/javascripts/fixtures/pdf.rb +++ /dev/null @@ -1,18 +0,0 @@ -require 'spec_helper' - -describe 'PDF file', '(JavaScript fixtures)', type: :controller do - include JavaScriptFixturesHelpers - - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} - let(:project) { create(:project, :repository, namespace: namespace, path: 'pdf-project') } - - before(:all) do - clean_frontend_fixtures('blob/pdf/') - end - - it 'blob/pdf/test.pdf' do |example| - blob = project.repository.blob_at('e774ebd33', 'files/pdf/test.pdf') - - store_frontend_fixture(blob.data.force_encoding("utf-8"), example.description) - end -end diff --git a/spec/javascripts/fixtures/pipeline_schedules.rb b/spec/javascripts/fixtures/pipeline_schedules.rb index e5176a58273..aecd56e6198 100644 --- a/spec/javascripts/fixtures/pipeline_schedules.rb +++ b/spec/javascripts/fixtures/pipeline_schedules.rb @@ -21,7 +21,7 @@ describe Projects::PipelineSchedulesController, '(JavaScript fixtures)', type: : sign_in(admin) end - it 'pipeline_schedules/edit.html' do |example| + it 'pipeline_schedules/edit.html' do get :edit, params: { namespace_id: project.namespace.to_param, project_id: project, @@ -29,10 +29,9 @@ describe Projects::PipelineSchedulesController, '(JavaScript fixtures)', type: : } expect(response).to be_success - store_frontend_fixture(response, example.description) end - it 'pipeline_schedules/edit_with_variables.html' do |example| + it 'pipeline_schedules/edit_with_variables.html' do get :edit, params: { namespace_id: project.namespace.to_param, project_id: project, @@ -40,6 +39,5 @@ describe Projects::PipelineSchedulesController, '(JavaScript fixtures)', type: : } expect(response).to be_success - store_frontend_fixture(response, example.description) end end diff --git a/spec/javascripts/fixtures/pipelines.rb b/spec/javascripts/fixtures/pipelines.rb index 42b552e81c0..de6fcfe10f4 100644 --- a/spec/javascripts/fixtures/pipelines.rb +++ b/spec/javascripts/fixtures/pipelines.rb @@ -23,13 +23,12 @@ describe Projects::PipelinesController, '(JavaScript fixtures)', type: :controll sign_in(admin) end - it 'pipelines/pipelines.json' do |example| + it 'pipelines/pipelines.json' do get :index, params: { namespace_id: namespace, project_id: project }, format: :json expect(response).to be_success - store_frontend_fixture(response, example.description) end end diff --git a/spec/javascripts/fixtures/projects.rb b/spec/javascripts/fixtures/projects.rb index 446da83a7f9..94c59207898 100644 --- a/spec/javascripts/fixtures/projects.rb +++ b/spec/javascripts/fixtures/projects.rb @@ -28,49 +28,45 @@ describe 'Projects (JavaScript fixtures)', type: :controller do end describe ProjectsController, '(JavaScript fixtures)', type: :controller do - it 'projects/dashboard.html' do |example| + it 'projects/dashboard.html' do get :show, params: { namespace_id: project.namespace.to_param, id: project } expect(response).to be_success - store_frontend_fixture(response, example.description) end - it 'projects/overview.html' do |example| + it 'projects/overview.html' do get :show, params: { namespace_id: project_with_repo.namespace.to_param, id: project_with_repo } expect(response).to be_success - store_frontend_fixture(response, example.description) end - it 'projects/edit.html' do |example| + it 'projects/edit.html' do get :edit, params: { namespace_id: project.namespace.to_param, id: project } expect(response).to be_success - store_frontend_fixture(response, example.description) end end describe Projects::Settings::CiCdController, '(JavaScript fixtures)', type: :controller do - it 'projects/ci_cd_settings.html' do |example| + it 'projects/ci_cd_settings.html' do get :show, params: { namespace_id: project.namespace.to_param, project_id: project } expect(response).to be_success - store_frontend_fixture(response, example.description) end - it 'projects/ci_cd_settings_with_variables.html' do |example| + it 'projects/ci_cd_settings_with_variables.html' do create(:ci_variable, project: project_variable_populated) create(:ci_variable, project: project_variable_populated) @@ -80,7 +76,6 @@ describe 'Projects (JavaScript fixtures)', type: :controller do } expect(response).to be_success - store_frontend_fixture(response, example.description) end end end diff --git a/spec/javascripts/fixtures/prometheus_service.rb b/spec/javascripts/fixtures/prometheus_service.rb index 29dc95305b7..f3171fdd97b 100644 --- a/spec/javascripts/fixtures/prometheus_service.rb +++ b/spec/javascripts/fixtures/prometheus_service.rb @@ -22,7 +22,7 @@ describe Projects::ServicesController, '(JavaScript fixtures)', type: :controlle remove_repository(project) end - it 'services/prometheus/prometheus_service.html' do |example| + it 'services/prometheus/prometheus_service.html' do get :edit, params: { namespace_id: namespace, project_id: project, @@ -30,6 +30,5 @@ describe Projects::ServicesController, '(JavaScript fixtures)', type: :controlle } expect(response).to be_success - store_frontend_fixture(response, example.description) end end diff --git a/spec/javascripts/fixtures/raw.rb b/spec/javascripts/fixtures/raw.rb index 82770beb39b..801c80a0112 100644 --- a/spec/javascripts/fixtures/raw.rb +++ b/spec/javascripts/fixtures/raw.rb @@ -1,34 +1,39 @@ require 'spec_helper' -describe 'Raw files', '(JavaScript fixtures)', type: :controller do +describe 'Raw files', '(JavaScript fixtures)' do include JavaScriptFixturesHelpers let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} let(:project) { create(:project, :repository, namespace: namespace, path: 'raw-project') } + let(:response) { @blob.data.force_encoding('UTF-8') } before(:all) do + clean_frontend_fixtures('blob/balsamiq/') clean_frontend_fixtures('blob/notebook/') + clean_frontend_fixtures('blob/pdf/') end after do remove_repository(project) end - it 'blob/notebook/basic.json' do |example| - blob = project.repository.blob_at('6d85bb69', 'files/ipython/basic.ipynb') - - store_frontend_fixture(blob.data, example.description) + it 'blob/balsamiq/test.bmpr' do + @blob = project.repository.blob_at('b89b56d79', 'files/images/balsamiq.bmpr') end - it 'blob/notebook/worksheets.json' do |example| - blob = project.repository.blob_at('6d85bb69', 'files/ipython/worksheets.ipynb') + it 'blob/notebook/basic.json' do + @blob = project.repository.blob_at('6d85bb69', 'files/ipython/basic.ipynb') + end - store_frontend_fixture(blob.data, example.description) + it 'blob/notebook/worksheets.json' do + @blob = project.repository.blob_at('6d85bb69', 'files/ipython/worksheets.ipynb') end - it 'blob/notebook/math.json' do |example| - blob = project.repository.blob_at('93ee732', 'files/ipython/math.ipynb') + it 'blob/notebook/math.json' do + @blob = project.repository.blob_at('93ee732', 'files/ipython/math.ipynb') + end - store_frontend_fixture(blob.data, example.description) + it 'blob/pdf/test.pdf' do + @blob = project.repository.blob_at('e774ebd33', 'files/pdf/test.pdf') end end diff --git a/spec/javascripts/fixtures/search.rb b/spec/javascripts/fixtures/search.rb index 5f5b4d4e60d..22fc546d761 100644 --- a/spec/javascripts/fixtures/search.rb +++ b/spec/javascripts/fixtures/search.rb @@ -9,10 +9,9 @@ describe SearchController, '(JavaScript fixtures)', type: :controller do clean_frontend_fixtures('search/') end - it 'search/show.html' do |example| + it 'search/show.html' do get :show expect(response).to be_success - store_frontend_fixture(response, example.description) end end diff --git a/spec/javascripts/fixtures/services.rb b/spec/javascripts/fixtures/services.rb index dc7ee484c22..2237702ccca 100644 --- a/spec/javascripts/fixtures/services.rb +++ b/spec/javascripts/fixtures/services.rb @@ -22,7 +22,7 @@ describe Projects::ServicesController, '(JavaScript fixtures)', type: :controlle remove_repository(project) end - it 'services/edit_service.html' do |example| + it 'services/edit_service.html' do get :edit, params: { namespace_id: namespace, project_id: project, @@ -30,6 +30,5 @@ describe Projects::ServicesController, '(JavaScript fixtures)', type: :controlle } expect(response).to be_success - store_frontend_fixture(response, example.description) end end diff --git a/spec/javascripts/fixtures/sessions.rb b/spec/javascripts/fixtures/sessions.rb index 8656dea696a..92b74c01c89 100644 --- a/spec/javascripts/fixtures/sessions.rb +++ b/spec/javascripts/fixtures/sessions.rb @@ -16,11 +16,10 @@ describe 'Sessions (JavaScript fixtures)' do set_devise_mapping(context: @request) end - it 'sessions/new.html' do |example| + it 'sessions/new.html' do get :new expect(response).to be_success - store_frontend_fixture(response, example.description) end end end diff --git a/spec/javascripts/fixtures/snippet.rb b/spec/javascripts/fixtures/snippet.rb index ebc5b793166..ace84b14eb7 100644 --- a/spec/javascripts/fixtures/snippet.rb +++ b/spec/javascripts/fixtures/snippet.rb @@ -23,12 +23,11 @@ describe SnippetsController, '(JavaScript fixtures)', type: :controller do remove_repository(project) end - it 'snippets/show.html' do |example| + it 'snippets/show.html' do create(:discussion_note_on_snippet, noteable: snippet, project: project, author: admin, note: '- [ ] Task List Item') get(:show, params: { id: snippet.to_param }) expect(response).to be_success - store_frontend_fixture(response, example.description) end end diff --git a/spec/javascripts/fixtures/todos.rb b/spec/javascripts/fixtures/todos.rb index 6e37a2e5a4c..d0c8a6eca01 100644 --- a/spec/javascripts/fixtures/todos.rb +++ b/spec/javascripts/fixtures/todos.rb @@ -26,11 +26,10 @@ describe 'Todos (JavaScript fixtures)' do sign_in(admin) end - it 'todos/todos.html' do |example| + it 'todos/todos.html' do get :index expect(response).to be_success - store_frontend_fixture(response, example.description) end end @@ -41,7 +40,7 @@ describe 'Todos (JavaScript fixtures)' do sign_in(admin) end - it 'todos/todos.json' do |example| + it 'todos/todos.json' do post :create, params: { namespace_id: namespace, project_id: project, @@ -50,7 +49,6 @@ describe 'Todos (JavaScript fixtures)' do }, format: 'json' expect(response).to be_success - store_frontend_fixture(response, example.description) end end end diff --git a/spec/javascripts/fixtures/u2f.rb b/spec/javascripts/fixtures/u2f.rb index 15866d65a4f..f52832b6efb 100644 --- a/spec/javascripts/fixtures/u2f.rb +++ b/spec/javascripts/fixtures/u2f.rb @@ -18,13 +18,12 @@ context 'U2F' do set_devise_mapping(context: @request) end - it 'u2f/authenticate.html' do |example| + it 'u2f/authenticate.html' do allow(controller).to receive(:find_user).and_return(user) post :create, params: { user: { login: user.username, password: user.password } } expect(response).to be_success - store_frontend_fixture(response, example.description) end end @@ -36,11 +35,10 @@ context 'U2F' do allow_any_instance_of(Profiles::TwoFactorAuthsController).to receive(:build_qr_code).and_return('qrcode:blackandwhitesquares') end - it 'u2f/register.html' do |example| + it 'u2f/register.html' do get :show expect(response).to be_success - store_frontend_fixture(response, example.description) end end end diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js index 7ef44f29c5b..4772f754937 100644 --- a/spec/javascripts/fly_out_nav_spec.js +++ b/spec/javascripts/fly_out_nav_spec.js @@ -14,6 +14,7 @@ import { setSidebar, subItemsMouseLeave, } from '~/fly_out_nav'; +import { SIDEBAR_COLLAPSED_CLASS } from '~/contextual_sidebar'; import bp from '~/breakpoints'; describe('Fly out sidebar navigation', () => { @@ -219,7 +220,7 @@ describe('Fly out sidebar navigation', () => { it('shows collapsed only sub-items if icon only sidebar', () => { const subItems = el.querySelector('.sidebar-sub-level-items'); const sidebar = document.createElement('div'); - sidebar.classList.add('sidebar-collapsed-desktop'); + sidebar.classList.add(SIDEBAR_COLLAPSED_CLASS); subItems.classList.add('is-fly-out-only'); setSidebar(sidebar); @@ -296,7 +297,7 @@ describe('Fly out sidebar navigation', () => { it('returns true when active & collapsed sidebar', () => { const sidebar = document.createElement('div'); - sidebar.classList.add('sidebar-collapsed-desktop'); + sidebar.classList.add(SIDEBAR_COLLAPSED_CLASS); el.classList.add('active'); setSidebar(sidebar); diff --git a/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js b/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js index f00bc2eeb6d..dce8e3be148 100644 --- a/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js +++ b/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue'; import { shallowMount } from '@vue/test-utils'; -import { trimText } from 'spec/helpers/vue_component_helper'; +import { trimText } from 'spec/helpers/text_helper'; import { mockProject } from '../mock_data'; // can also use 'mockGroup', but not useful to test here const createComponent = () => { diff --git a/spec/javascripts/helpers/vue_component_helper.js b/spec/javascripts/helpers/text_helper.js index e0fe18e5560..e0fe18e5560 100644 --- a/spec/javascripts/helpers/vue_component_helper.js +++ b/spec/javascripts/helpers/text_helper.js diff --git a/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js index 9af3c15a4e3..3c7d6192e2c 100644 --- a/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js @@ -3,7 +3,7 @@ import store from '~/ide/stores'; import listCollapsed from '~/ide/components/commit_sidebar/list_collapsed.vue'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { file } from '../../helpers'; -import { removeWhitespace } from '../../../helpers/vue_component_helper'; +import { removeWhitespace } from '../../../helpers/text_helper'; describe('Multi-file editor commit sidebar list collapsed', () => { let vm; diff --git a/spec/javascripts/ide/components/ide_review_spec.js b/spec/javascripts/ide/components/ide_review_spec.js index b9ee22b7c1a..396c5d282d4 100644 --- a/spec/javascripts/ide/components/ide_review_spec.js +++ b/spec/javascripts/ide/components/ide_review_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import IdeReview from '~/ide/components/ide_review.vue'; import store from '~/ide/stores'; import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; -import { trimText } from '../../helpers/vue_component_helper'; +import { trimText } from '../../helpers/text_helper'; import { resetStore, file } from '../helpers'; import { projectData } from '../mock_data'; diff --git a/spec/javascripts/ide/components/nav_dropdown_button_spec.js b/spec/javascripts/ide/components/nav_dropdown_button_spec.js index 0a58e260280..19b0071567a 100644 --- a/spec/javascripts/ide/components/nav_dropdown_button_spec.js +++ b/spec/javascripts/ide/components/nav_dropdown_button_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import NavDropdownButton from '~/ide/components/nav_dropdown_button.vue'; import store from '~/ide/stores'; -import { trimText } from 'spec/helpers/vue_component_helper'; +import { trimText } from 'spec/helpers/text_helper'; import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { resetStore } from '../helpers'; diff --git a/spec/javascripts/jobs/components/artifacts_block_spec.js b/spec/javascripts/jobs/components/artifacts_block_spec.js index 27d480ef2ea..58998d038e5 100644 --- a/spec/javascripts/jobs/components/artifacts_block_spec.js +++ b/spec/javascripts/jobs/components/artifacts_block_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import { getTimeago } from '~/lib/utils/datetime_utility'; import component from '~/jobs/components/artifacts_block.vue'; import mountComponent from '../../helpers/vue_mount_component_helper'; -import { trimText } from '../../helpers/vue_component_helper'; +import { trimText } from '../../helpers/text_helper'; describe('Artifacts block', () => { const Component = Vue.extend(component); diff --git a/spec/javascripts/jobs/components/sidebar_spec.js b/spec/javascripts/jobs/components/sidebar_spec.js index 3a02351460c..26d9effcac5 100644 --- a/spec/javascripts/jobs/components/sidebar_spec.js +++ b/spec/javascripts/jobs/components/sidebar_spec.js @@ -3,7 +3,7 @@ import sidebarDetailsBlock from '~/jobs/components/sidebar.vue'; import createStore from '~/jobs/store'; import job, { stages, jobsInStage } from '../mock_data'; import { mountComponentWithStore } from '../../helpers/vue_mount_component_helper'; -import { trimText } from '../../helpers/vue_component_helper'; +import { trimText } from '../../helpers/text_helper'; describe('Sidebar details block', () => { const SidebarComponent = Vue.extend(sidebarDetailsBlock); diff --git a/spec/javascripts/jobs/components/stages_dropdown_spec.js b/spec/javascripts/jobs/components/stages_dropdown_spec.js index eccb4e13d67..52bb5161123 100644 --- a/spec/javascripts/jobs/components/stages_dropdown_spec.js +++ b/spec/javascripts/jobs/components/stages_dropdown_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import component from '~/jobs/components/stages_dropdown.vue'; -import { trimText } from 'spec/helpers/vue_component_helper'; +import { trimText } from 'spec/helpers/text_helper'; import mountComponent from '../../helpers/vue_mount_component_helper'; describe('Stages Dropdown', () => { diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js index 5c28840d3a4..e9bd6050d68 100644 --- a/spec/javascripts/monitoring/dashboard_spec.js +++ b/spec/javascripts/monitoring/dashboard_spec.js @@ -20,6 +20,9 @@ const propsData = { emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg', environmentsEndpoint: '/root/hello-prometheus/environments/35', currentEnvironmentName: 'production', + customMetricsAvailable: false, + customMetricsPath: '', + validateQueryPath: '', }; export default propsData; @@ -37,6 +40,9 @@ describe('Dashboard', () => { window.gon = { ...window.gon, ee: false, + features: { + grafanaDashboardLink: true, + }, }; mock = new MockAdapter(axios); @@ -160,7 +166,7 @@ describe('Dashboard', () => { }); }); - it('renders the environments dropdown with a single is-active element', done => { + it('renders the environments dropdown with a single active element', done => { const component = new DashboardComponent({ el: document.querySelector('.prometheus-graphs'), propsData: { @@ -175,7 +181,7 @@ describe('Dashboard', () => { setTimeout(() => { const dropdownItems = component.$el.querySelectorAll( - '.js-environments-dropdown .dropdown-item.is-active', + '.js-environments-dropdown .dropdown-item[active="true"]', ); expect(dropdownItems.length).toEqual(1); @@ -323,4 +329,63 @@ describe('Dashboard', () => { .catch(done.fail); }); }); + + describe('external dashboard link', () => { + let component; + + beforeEach(() => { + mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse); + }); + + afterEach(() => { + component.$destroy(); + }); + + describe('with feature flag enabled', () => { + beforeEach(() => { + component = new DashboardComponent({ + el: document.querySelector('.prometheus-graphs'), + propsData: { + ...propsData, + hasMetrics: true, + showPanels: false, + showTimeWindowDropdown: false, + externalDashboardPath: '/mockPath', + }, + }); + }); + + it('shows the link', done => { + setTimeout(() => { + expect(component.$el.querySelector('.js-external-dashboard-link').innerText).toContain( + 'View full dashboard', + ); + done(); + }); + }); + }); + + describe('without feature flage enabled', () => { + beforeEach(() => { + window.gon.features.grafanaDashboardLink = false; + component = new DashboardComponent({ + el: document.querySelector('.prometheus-graphs'), + propsData: { + ...propsData, + hasMetrics: true, + showPanels: false, + showTimeWindowDropdown: false, + externalDashboardPath: '', + }, + }); + }); + + it('does not show the link', done => { + setTimeout(() => { + expect(component.$el.querySelector('.js-external-dashboard-link')).toBe(null); + done(); + }); + }); + }); + }); }); diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index 94ce6d8e222..7a9f32ddcff 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -3,11 +3,12 @@ import $ from 'jquery'; import _ from 'underscore'; import { TEST_HOST } from 'spec/test_constants'; import { headersInterceptor } from 'spec/helpers/vue_resource_helper'; -import * as actions from '~/notes/stores/actions'; +import actionsModule, * as actions from '~/notes/stores/actions'; import * as mutationTypes from '~/notes/stores/mutation_types'; import * as notesConstants from '~/notes/constants'; import createStore from '~/notes/stores'; import mrWidgetEventHub from '~/vue_merge_request_widget/event_hub'; +import service from '~/notes/services/notes_service'; import testAction from '../../helpers/vuex_action_helper'; import { resetStore } from '../helpers'; import { @@ -18,11 +19,21 @@ import { individualNote, } from '../mock_data'; +const TEST_ERROR_MESSAGE = 'Test error message'; + describe('Actions Notes Store', () => { + let commit; + let dispatch; + let state; let store; + let flashSpy; beforeEach(() => { store = createStore(); + commit = jasmine.createSpy('commit'); + dispatch = jasmine.createSpy('dispatch'); + state = {}; + flashSpy = spyOnDependency(actionsModule, 'Flash'); }); afterEach(() => { @@ -604,21 +615,6 @@ describe('Actions Notes Store', () => { }); describe('updateOrCreateNotes', () => { - let commit; - let dispatch; - let state; - - beforeEach(() => { - commit = jasmine.createSpy('commit'); - dispatch = jasmine.createSpy('dispatch'); - state = {}; - }); - - afterEach(() => { - commit.calls.reset(); - dispatch.calls.reset(); - }); - it('Updates existing note', () => { const note = { id: 1234 }; const getters = { notesById: { 1234: note } }; @@ -751,4 +747,151 @@ describe('Actions Notes Store', () => { ); }); }); + + describe('resolveDiscussion', () => { + let getters; + let discussionId; + + beforeEach(() => { + discussionId = discussionMock.id; + state.discussions = [discussionMock]; + getters = { + isDiscussionResolved: () => false, + }; + }); + + it('when unresolved, dispatches action', done => { + testAction( + actions.resolveDiscussion, + { discussionId }, + { ...state, ...getters }, + [], + [ + { + type: 'toggleResolveNote', + payload: { + endpoint: discussionMock.resolve_path, + isResolved: false, + discussion: true, + }, + }, + ], + done, + ); + }); + + it('when resolved, does nothing', done => { + getters.isDiscussionResolved = id => id === discussionId; + + testAction( + actions.resolveDiscussion, + { discussionId }, + { ...state, ...getters }, + [], + [], + done, + ); + }); + }); + + describe('saveNote', () => { + const payload = { endpoint: TEST_HOST, data: { 'note[note]': 'some text' } }; + + describe('if response contains errors', () => { + const res = { errors: { something: ['went wrong'] } }; + + it('throws an error', done => { + actions + .saveNote( + { + commit() {}, + dispatch: () => Promise.resolve(res), + }, + payload, + ) + .then(() => done.fail('Expected error to be thrown!')) + .catch(error => { + expect(error.message).toBe('Failed to save comment!'); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('if response contains no errors', () => { + const res = { valid: true }; + + it('returns the response', done => { + actions + .saveNote( + { + commit() {}, + dispatch: () => Promise.resolve(res), + }, + payload, + ) + .then(data => { + expect(data).toBe(res); + }) + .then(done) + .catch(done.fail); + }); + }); + }); + + describe('submitSuggestion', () => { + const discussionId = 'discussion-id'; + const noteId = 'note-id'; + const suggestionId = 'suggestion-id'; + let flashContainer; + + beforeEach(() => { + spyOn(service, 'applySuggestion'); + dispatch.and.returnValue(Promise.resolve()); + service.applySuggestion.and.returnValue(Promise.resolve()); + flashContainer = {}; + }); + + const testSubmitSuggestion = (done, expectFn) => { + actions + .submitSuggestion( + { commit, dispatch }, + { discussionId, noteId, suggestionId, flashContainer }, + ) + .then(expectFn) + .then(done) + .catch(done.fail); + }; + + it('when service success, commits and resolves discussion', done => { + testSubmitSuggestion(done, () => { + expect(commit.calls.allArgs()).toEqual([ + [mutationTypes.APPLY_SUGGESTION, { discussionId, noteId, suggestionId }], + ]); + + expect(dispatch.calls.allArgs()).toEqual([['resolveDiscussion', { discussionId }]]); + expect(flashSpy).not.toHaveBeenCalled(); + }); + }); + + it('when service fails, flashes error message', done => { + const response = { response: { data: { message: TEST_ERROR_MESSAGE } } }; + + service.applySuggestion.and.returnValue(Promise.reject(response)); + + testSubmitSuggestion(done, () => { + expect(commit).not.toHaveBeenCalled(); + expect(dispatch).not.toHaveBeenCalled(); + expect(flashSpy).toHaveBeenCalledWith(`${TEST_ERROR_MESSAGE}.`, 'alert', flashContainer); + }); + }); + + it('when resolve discussion fails, fail gracefully', done => { + dispatch.and.returnValue(Promise.reject()); + + testSubmitSuggestion(done, () => { + expect(flashSpy).not.toHaveBeenCalled(); + }); + }); + }); }); diff --git a/spec/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js index a89952ee435..5f4dba5ecb9 100644 --- a/spec/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js +++ b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js @@ -3,6 +3,7 @@ import GLDropdown from '~/gl_dropdown'; // eslint-disable-line no-unused-vars import TimezoneDropdown, { formatUtcOffset, formatTimezone, + findTimezoneByIdentifier, } from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown'; describe('Timezone Dropdown', function() { @@ -12,6 +13,7 @@ describe('Timezone Dropdown', function() { let $dropdownEl = null; let $wrapper = null; const tzListSel = '.dropdown-content ul li a.is-active'; + const tzDropdownToggleText = '.dropdown-toggle-text'; describe('Initialize', () => { describe('with dropdown already loaded', () => { @@ -94,6 +96,36 @@ describe('Timezone Dropdown', function() { expect(onSelectTimezone).toHaveBeenCalled(); }); + + it('will correctly set the dropdown label if a timezone identifier is set on the inputEl', () => { + $inputEl.val('America/St_Johns'); + + // eslint-disable-next-line no-new + new TimezoneDropdown({ + $inputEl, + $dropdownEl, + displayFormat: selectedItem => formatTimezone(selectedItem), + }); + + expect($wrapper.find(tzDropdownToggleText).html()).toEqual('[UTC - 2.5] Newfoundland'); + }); + + it('will call a provided `displayFormat` handler to format the dropdown value', () => { + const displayFormat = jasmine.createSpy('displayFormat'); + // eslint-disable-next-line no-new + new TimezoneDropdown({ + $inputEl, + $dropdownEl, + displayFormat, + }); + + $wrapper + .find(tzListSel) + .first() + .trigger('click'); + + expect(displayFormat).toHaveBeenCalled(); + }); }); }); @@ -164,4 +196,49 @@ describe('Timezone Dropdown', function() { ).toEqual('[UTC 0] Accra'); }); }); + + describe('findTimezoneByIdentifier', () => { + const tzList = [ + { + identifier: 'Asia/Tokyo', + name: 'Sapporo', + offset: 32400, + }, + { + identifier: 'Asia/Hong_Kong', + name: 'Hong Kong', + offset: 28800, + }, + { + identifier: 'Asia/Dhaka', + name: 'Dhaka', + offset: 21600, + }, + ]; + + const identifier = 'Asia/Dhaka'; + it('returns the correct object if the identifier exists', () => { + const res = findTimezoneByIdentifier(tzList, identifier); + + expect(res).toBeTruthy(); + expect(res).toBe(tzList[2]); + }); + + it('returns null if it doesnt find the identifier', () => { + const res = findTimezoneByIdentifier(tzList, 'Australia/Melbourne'); + + expect(res).toBeNull(); + }); + + it('returns null if there is no identifier given', () => { + expect(findTimezoneByIdentifier(tzList)).toBeNull(); + expect(findTimezoneByIdentifier(tzList, '')).toBeNull(); + }); + + it('returns null if there is an empty or invalid array given', () => { + expect(findTimezoneByIdentifier([], identifier)).toBeNull(); + expect(findTimezoneByIdentifier(null, identifier)).toBeNull(); + expect(findTimezoneByIdentifier(undefined, identifier)).toBeNull(); + }); + }); }); diff --git a/spec/javascripts/reports/components/modal_spec.js b/spec/javascripts/reports/components/modal_spec.js index 6b8471381de..d42c509e5b5 100644 --- a/spec/javascripts/reports/components/modal_spec.js +++ b/spec/javascripts/reports/components/modal_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import component from '~/reports/components/modal.vue'; import state from '~/reports/store/state'; import mountComponent from '../../helpers/vue_mount_component_helper'; -import { trimText } from '../../helpers/vue_component_helper'; +import { trimText } from '../../helpers/text_helper'; describe('Grouped Test Reports Modal', () => { const Component = Vue.extend(component); diff --git a/spec/javascripts/reports/components/test_issue_body_spec.js b/spec/javascripts/reports/components/test_issue_body_spec.js index 32baf904ad7..9c1cec4c9bc 100644 --- a/spec/javascripts/reports/components/test_issue_body_spec.js +++ b/spec/javascripts/reports/components/test_issue_body_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import component from '~/reports/components/test_issue_body.vue'; import createStore from '~/reports/store'; import { mountComponentWithStore } from '../../helpers/vue_mount_component_helper'; -import { trimText } from '../../helpers/vue_component_helper'; +import { trimText } from '../../helpers/text_helper'; import { issue } from '../mock_data/mock_data'; describe('Test Issue body', () => { diff --git a/spec/javascripts/test_constants.js b/spec/javascripts/test_constants.js index 24b5512b053..77c206585fe 100644 --- a/spec/javascripts/test_constants.js +++ b/spec/javascripts/test_constants.js @@ -1,4 +1,6 @@ -export const FIXTURES_PATH = '/base/spec/javascripts/fixtures'; +export const FIXTURES_PATH = `/base/${ + process.env.IS_GITLAB_EE ? 'ee/' : '' +}spec/javascripts/fixtures`; export const TEST_HOST = 'http://test.host'; export const DUMMY_IMAGE_URL = `${FIXTURES_PATH}/static/images/one_white_pixel.png`; diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js index e5155573f6f..dfbc68c48b9 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js @@ -1,4 +1,4 @@ -import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { mount, createLocalVue } from '@vue/test-utils'; import MrWidgetPipelineContainer from '~/vue_merge_request_widget/components/mr_widget_pipeline_container.vue'; import MrWidgetPipeline from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue'; import { mockStore } from '../mock_data'; @@ -9,7 +9,7 @@ describe('MrWidgetPipelineContainer', () => { const factory = (props = {}) => { const localVue = createLocalVue(); - wrapper = shallowMount(localVue.extend(MrWidgetPipelineContainer), { + wrapper = mount(localVue.extend(MrWidgetPipelineContainer), { propsData: { mr: Object.assign({}, mockStore), ...props, diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js index de213210cfc..75017d20473 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import { trimText } from 'spec/helpers/vue_component_helper'; +import { trimText } from 'spec/helpers/text_helper'; import mockData from '../mock_data'; describe('MRWidgetPipeline', () => { @@ -78,6 +78,19 @@ describe('MRWidgetPipeline', () => { ); }); + it('should render CI error when no pipeline is provided', () => { + vm = mountComponent(Component, { + pipeline: {}, + hasCi: true, + ciStatus: 'success', + troubleshootingDocsPath: 'help', + }); + + expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain( + 'Could not retrieve the pipeline status. For troubleshooting steps, read the documentation.', + ); + }); + describe('with a pipeline', () => { beforeEach(() => { vm = mountComponent(Component, { diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js index 0ddbdf67d8b..39b879612ae 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js @@ -1,7 +1,7 @@ import $ from 'jquery'; import { createLocalVue, shallowMount } from '@vue/test-utils'; import ConflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts.vue'; -import { removeBreakLine } from 'spec/helpers/vue_component_helper'; +import { removeBreakLine } from 'spec/helpers/text_helper'; describe('MRWidgetConflicts', () => { let vm; diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js index 477041fa383..1d2f3e41509 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import pipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import { removeBreakLine } from 'spec/helpers/vue_component_helper'; +import { removeBreakLine } from 'spec/helpers/text_helper'; describe('MRWidgetPipelineBlocked', () => { let vm; diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js index f7523a01963..3e4ce2c3696 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import PipelineFailed from '~/vue_merge_request_widget/components/states/pipeline_failed.vue'; -import { removeBreakLine } from 'spec/helpers/vue_component_helper'; +import { removeBreakLine } from 'spec/helpers/text_helper'; describe('PipelineFailed', () => { describe('template', () => { diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js index 36f8c7a9683..9324c83bf4b 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import ShaMismatch from '~/vue_merge_request_widget/components/states/sha_mismatch.vue'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import { removeBreakLine } from 'spec/helpers/vue_component_helper'; +import { removeBreakLine } from 'spec/helpers/text_helper'; describe('ShaMismatch', () => { let vm; diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js index dda16375103..bec16b0aab0 100644 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -235,11 +235,44 @@ export default { troubleshooting_docs_path: 'help', merge_request_pipelines_docs_path: '/help/ci/merge_request_pipelines/index.md', squash: true, + visual_review_app_available: true, }; export const mockStore = { - pipeline: { id: 0 }, - mergePipeline: { id: 1 }, + pipeline: { + id: 0, + details: { + status: { + details_path: '/root/review-app-tester/pipelines/66', + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2. png', + group: 'success-with-warnings', + has_details: true, + icon: 'status_warning', + illustration: null, + label: 'passed with warnings', + text: 'passed', + tooltip: 'passed', + }, + }, + }, + mergePipeline: { + id: 1, + details: { + status: { + details_path: '/root/review-app-tester/pipelines/66', + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2. png', + group: 'success-with-warnings', + has_details: true, + icon: 'status_warning', + illustration: null, + label: 'passed with warnings', + text: 'passed', + tooltip: 'passed', + }, + }, + }, targetBranch: 'target-branch', sourceBranch: 'source-branch', sourceBranchLink: 'source-branch-link', diff --git a/spec/javascripts/vue_shared/components/markdown/suggestion_diff_header_spec.js b/spec/javascripts/vue_shared/components/markdown/suggestion_diff_header_spec.js deleted file mode 100644 index 12ee804f668..00000000000 --- a/spec/javascripts/vue_shared/components/markdown/suggestion_diff_header_spec.js +++ /dev/null @@ -1,75 +0,0 @@ -import Vue from 'vue'; -import SuggestionDiffHeaderComponent from '~/vue_shared/components/markdown/suggestion_diff_header.vue'; - -const MOCK_DATA = { - canApply: true, - isApplied: false, - helpPagePath: 'path_to_docs', -}; - -describe('Suggestion Diff component', () => { - let vm; - - function createComponent(propsData) { - const Component = Vue.extend(SuggestionDiffHeaderComponent); - - return new Component({ - propsData, - }).$mount(); - } - - beforeEach(done => { - vm = createComponent(MOCK_DATA); - Vue.nextTick(done); - }); - - describe('init', () => { - it('renders a suggestion header', () => { - const header = vm.$el.querySelector('.qa-suggestion-diff-header'); - - expect(header).not.toBeNull(); - expect(header.innerHTML.includes('Suggested change')).toBe(true); - }); - - it('renders a help button', () => { - const helpBtn = vm.$el.querySelector('.js-help-btn'); - - expect(helpBtn).not.toBeNull(); - }); - - it('renders an apply button', () => { - const applyBtn = vm.$el.querySelector('.qa-apply-btn'); - - expect(applyBtn).not.toBeNull(); - expect(applyBtn.innerHTML.includes('Apply suggestion')).toBe(true); - }); - - it('does not render an apply button if `canApply` is set to false', () => { - const props = Object.assign(MOCK_DATA, { canApply: false }); - - vm = createComponent(props); - - expect(vm.$el.querySelector('.qa-apply-btn')).toBeNull(); - }); - }); - - describe('applySuggestion', () => { - it('emits when the apply button is clicked', () => { - const props = Object.assign(MOCK_DATA, { canApply: true }); - - vm = createComponent(props); - spyOn(vm, '$emit'); - vm.applySuggestion(); - - expect(vm.$emit).toHaveBeenCalled(); - }); - - it('does not emit when the canApply is set to false', () => { - spyOn(vm, '$emit'); - vm.canApply = false; - vm.applySuggestion(); - - expect(vm.$emit).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js b/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js index 268ced38f40..47964a1702a 100644 --- a/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js +++ b/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js @@ -1,6 +1,6 @@ import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue'; import { shallowMount, createLocalVue } from '@vue/test-utils'; -import { trimText } from 'spec/helpers/vue_component_helper'; +import { trimText } from 'spec/helpers/text_helper'; const localVue = createLocalVue(); diff --git a/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js b/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js index 34c0cd435cd..7f5f1a778d7 100644 --- a/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js +++ b/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js @@ -3,7 +3,7 @@ import _ from 'underscore'; import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue'; import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue'; import { shallowMount } from '@vue/test-utils'; -import { trimText } from 'spec/helpers/vue_component_helper'; +import { trimText } from 'spec/helpers/text_helper'; describe('ProjectSelector component', () => { let wrapper; @@ -99,7 +99,7 @@ describe('ProjectSelector component', () => { expect(trimText(noResultsEl.text())).toEqual('Sorry, no projects matched your search'); }); - it(`shows a "minimum seach query" message if showMinimumSearchQueryMessage === true`, () => { + it(`shows a "minimum search query" message if showMinimumSearchQueryMessage === true`, () => { wrapper.setProps({ showMinimumSearchQueryMessage: true }); expect(wrapper.contains('.js-minimum-search-query-message')).toBe(true); diff --git a/spec/javascripts/vue_shared/translate_spec.js b/spec/javascripts/vue_shared/translate_spec.js index adb5ff682f0..0aaa4050cba 100644 --- a/spec/javascripts/vue_shared/translate_spec.js +++ b/spec/javascripts/vue_shared/translate_spec.js @@ -3,7 +3,7 @@ import Jed from 'jed'; import locale from '~/locale'; import Translate from '~/vue_shared/translate'; -import { trimText } from 'spec/helpers/vue_component_helper'; +import { trimText } from 'spec/helpers/text_helper'; describe('Vue translate filter', () => { let el; diff --git a/spec/lib/api/helpers/related_resources_helpers_spec.rb b/spec/lib/api/helpers/related_resources_helpers_spec.rb index 66af7f81535..99fe8795d91 100644 --- a/spec/lib/api/helpers/related_resources_helpers_spec.rb +++ b/spec/lib/api/helpers/related_resources_helpers_spec.rb @@ -5,6 +5,40 @@ describe API::Helpers::RelatedResourcesHelpers do Class.new.include(described_class).new end + describe '#expose_path' do + let(:path) { '/api/v4/awesome_endpoint' } + + context 'empty relative URL root' do + before do + stub_config_setting(relative_url_root: '') + end + + it 'returns the existing path' do + expect(helpers.expose_path(path)).to eq(path) + end + end + + context 'slash relative URL root' do + before do + stub_config_setting(relative_url_root: '/') + end + + it 'returns the existing path' do + expect(helpers.expose_path(path)).to eq(path) + end + end + + context 'with relative URL root' do + before do + stub_config_setting(relative_url_root: '/gitlab/root') + end + + it 'returns the existing path' do + expect(helpers.expose_path(path)).to eq("/gitlab/root" + path) + end + end + end + describe '#expose_url' do let(:path) { '/api/v4/awesome_endpoint' } subject(:url) { helpers.expose_url(path) } diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb index 05057789cc1..80ca7a63435 100644 --- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb +++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb @@ -80,7 +80,7 @@ describe Banzai::Filter::SyntaxHighlightFilter do let(:lang) { 'suggestion' } let(:lang_params) { '-1+10' } - it "delimits on the first appearence" do + it "delimits on the first appearance" do result = filter(%{<pre><code lang="#{lang}#{delimiter}#{lang_params}#{delimiter}more-things">This is a test</code></pre>}) expect(result.to_html).to eq(%{<pre class="code highlight js-syntax-highlight #{lang}" lang="#{lang}" #{data_attr}="#{lang_params}#{delimiter}more-things" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>}) diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb index e1a2bae5fe8..a02c00e3340 100644 --- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb @@ -222,6 +222,46 @@ describe Gitlab::BitbucketImport::Importer do body: {}.to_json) end + context 'creating labels on project' do + before do + allow(importer).to receive(:import_wiki) + end + + it 'creates labels as expected' do + expect { importer.execute }.to change { Label.count }.from(0).to(Gitlab::BitbucketImport::Importer::LABELS.size) + end + + it 'does not fail if label is already existing' do + label = Gitlab::BitbucketImport::Importer::LABELS.first + ::Labels::CreateService.new(label).execute(project: project) + + expect { importer.execute }.not_to raise_error + end + + it 'does not create new labels' do + Gitlab::BitbucketImport::Importer::LABELS.each do |label| + create(:label, project: project, title: label[:title]) + end + + expect { importer.execute }.not_to change { Label.count } + end + + it 'does not update existing ones' do + label_title = Gitlab::BitbucketImport::Importer::LABELS.first[:title] + existing_label = create(:label, project: project, title: label_title) + # Reload label from database so we avoid timestamp comparison issues related to time precision when comparing + # attributes later. + existing_label.reload + + Timecop.freeze(Time.now + 1.minute) do + importer.execute + + label_after_import = project.labels.find(existing_label.id) + expect(label_after_import.attributes).to eq(existing_label.attributes) + end + end + end + it 'maps statuses to open or closed' do allow(importer).to receive(:import_wiki) diff --git a/spec/lib/gitlab/ci/cron_parser_spec.rb b/spec/lib/gitlab/ci/cron_parser_spec.rb index 2a3f7807fdb..491e3fba9d9 100644 --- a/spec/lib/gitlab/ci/cron_parser_spec.rb +++ b/spec/lib/gitlab/ci/cron_parser_spec.rb @@ -174,6 +174,13 @@ describe Gitlab::Ci::CronParser do it { expect(subject).to be_nil } end + + context 'when cron is scheduled to a non existent day' do + let(:cron) { '0 12 31 2 *' } + let(:cron_timezone) { 'UTC' } + + it { expect(subject).to be_nil } + end end describe '#cron_valid?' do diff --git a/spec/lib/gitlab/favicon_spec.rb b/spec/lib/gitlab/favicon_spec.rb index 49a423191bb..dce56bbd2c4 100644 --- a/spec/lib/gitlab/favicon_spec.rb +++ b/spec/lib/gitlab/favicon_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Gitlab::Favicon, :request_store do expect(described_class.main).to match_asset_path '/assets/favicon.png' end - it 'has blue favicon for development' do + it 'has blue favicon for development', unless: Gitlab.ee? do allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development')) expect(described_class.main).to match_asset_path '/assets/favicon-blue.png' end diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb index 7ad3cde97f8..7e169cfe270 100644 --- a/spec/lib/gitlab/git/tree_spec.rb +++ b/spec/lib/gitlab/git/tree_spec.rb @@ -19,7 +19,9 @@ describe Gitlab::Git::Tree, :seed_helper do it 'returns a list of tree objects' do entries = described_class.where(repository, SeedRepo::Commit::ID, 'files', true) - expect(entries.count).to be >= 5 + expect(entries.map(&:path)).to include('files/html', + 'files/markdown/ruby-style-guide.md') + expect(entries.count).to be >= 10 expect(entries).to all(be_a(Gitlab::Git::Tree)) end diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index cfc3e0ce926..bc4f867e891 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -91,7 +91,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver do end it 'has issue comments' do - expect(saved_project_json['issues'].first['notes']).not_to be_empty + notes = saved_project_json['issues'].first['notes'] + + expect(notes).not_to be_empty + expect(notes.first['type']).to eq('DiscussionNote') end it 'has issue assignees' do @@ -299,7 +302,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver do create(:commit_status, project: project, pipeline: ci_build.pipeline) create(:milestone, project: project) - create(:note, noteable: issue, project: project) + create(:discussion_note, noteable: issue, project: project) create(:note, noteable: merge_request, project: project) create(:note, noteable: snippet, project: project) create(:note_on_commit, diff --git a/spec/lib/gitlab/lets_encrypt/challenge_spec.rb b/spec/lib/gitlab/lets_encrypt/challenge_spec.rb new file mode 100644 index 00000000000..74622f356de --- /dev/null +++ b/spec/lib/gitlab/lets_encrypt/challenge_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ::Gitlab::LetsEncrypt::Challenge do + delegated_methods = { + url: 'https://example.com/', + status: 'pending', + token: 'tokenvalue', + file_content: 'hereisfilecontent', + request_validation: true + } + + let(:acme_challenge) do + acme_challenge = instance_double('Acme::Client::Resources::Challenge') + allow(acme_challenge).to receive_messages(delegated_methods) + acme_challenge + end + + let(:challenge) { described_class.new(acme_challenge) } + + delegated_methods.each do |method, value| + describe "##{method}" do + it 'delegates to Acme::Client::Resources::Challenge' do + expect(challenge.public_send(method)).to eq(value) + end + end + end +end diff --git a/spec/lib/gitlab/lets_encrypt/client_spec.rb b/spec/lib/gitlab/lets_encrypt/client_spec.rb new file mode 100644 index 00000000000..16a16acfd25 --- /dev/null +++ b/spec/lib/gitlab/lets_encrypt/client_spec.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ::Gitlab::LetsEncrypt::Client do + include LetsEncryptHelpers + + let(:client) { described_class.new } + + before do + stub_application_setting( + lets_encrypt_notification_email: 'myemail@test.example.com', + lets_encrypt_terms_of_service_accepted: true + ) + end + + let!(:stub_client) { stub_lets_encrypt_client } + + shared_examples 'ensures account registration' do + it 'ensures account registration' do + subject + + expect(stub_client).to have_received(:new_account).with( + contact: 'mailto:myemail@test.example.com', + terms_of_service_agreed: true + ) + end + + context 'when acme integration is disabled' do + before do + stub_application_setting(lets_encrypt_terms_of_service_accepted: false) + end + + it 'raises error' do + expect do + subject + end.to raise_error('Acme integration is disabled') + end + end + end + + describe '#new_order' do + subject(:new_order) { client.new_order('example.com') } + + before do + order_double = instance_double('Acme::Order') + allow(stub_client).to receive(:new_order).and_return(order_double) + end + + include_examples 'ensures account registration' + + it 'returns order' do + is_expected.to be_a(::Gitlab::LetsEncrypt::Order) + end + end + + describe '#load_order' do + let(:url) { 'https://example.com/order' } + subject { client.load_order(url) } + + before do + acme_order = instance_double('Acme::Client::Resources::Order') + allow(stub_client).to receive(:order).with(url: url).and_return(acme_order) + end + + include_examples 'ensures account registration' + + it 'loads order' do + is_expected.to be_a(::Gitlab::LetsEncrypt::Order) + end + end + + describe '#load_challenge' do + let(:url) { 'https://example.com/challenge' } + subject { client.load_challenge(url) } + + before do + acme_challenge = instance_double('Acme::Client::Resources::Challenge') + allow(stub_client).to receive(:challenge).with(url: url).and_return(acme_challenge) + end + + include_examples 'ensures account registration' + + it 'loads challenge' do + is_expected.to be_a(::Gitlab::LetsEncrypt::Challenge) + end + end + + describe '#enabled?' do + subject { client.enabled? } + + context 'when terms of service are accepted' do + it { is_expected.to eq(true) } + + context 'when feature flag is disabled' do + before do + stub_feature_flags(pages_auto_ssl: false) + end + + it { is_expected.to eq(false) } + end + end + + context 'when terms of service are not accepted' do + before do + stub_application_setting(lets_encrypt_terms_of_service_accepted: false) + end + + it { is_expected.to eq(false) } + end + end + + describe '#terms_of_service_url' do + subject { client.terms_of_service_url } + + it 'returns valid url' do + is_expected.to eq("https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf") + end + end +end diff --git a/spec/lib/gitlab/lets_encrypt/order_spec.rb b/spec/lib/gitlab/lets_encrypt/order_spec.rb new file mode 100644 index 00000000000..ee7058baf8d --- /dev/null +++ b/spec/lib/gitlab/lets_encrypt/order_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ::Gitlab::LetsEncrypt::Order do + delegated_methods = { + url: 'https://example.com/', + status: 'valid' + } + + let(:acme_order) do + acme_order = instance_double('Acme::Client::Resources::Order') + allow(acme_order).to receive_messages(delegated_methods) + acme_order + end + + let(:order) { described_class.new(acme_order) } + + delegated_methods.each do |method, value| + describe "##{method}" do + it 'delegates to Acme::Client::Resources::Order' do + expect(order.public_send(method)).to eq(value) + end + end + end + + describe '#new_challenge' do + before do + challenge = instance_double('Acme::Client::Resources::Challenges::HTTP01') + authorization = instance_double('Acme::Client::Resources::Authorization') + allow(authorization).to receive(:http).and_return(challenge) + allow(acme_order).to receive(:authorizations).and_return([authorization]) + end + + it 'returns challenge' do + expect(order.new_challenge).to be_a(::Gitlab::LetsEncrypt::Challenge) + end + end +end diff --git a/spec/lib/gitlab/sentry_spec.rb b/spec/lib/gitlab/sentry_spec.rb index ae522a588ee..af8b059b984 100644 --- a/spec/lib/gitlab/sentry_spec.rb +++ b/spec/lib/gitlab/sentry_spec.rb @@ -2,12 +2,15 @@ require 'spec_helper' describe Gitlab::Sentry do describe '.context' do - it 'adds the locale to the tags' do + it 'adds the expected tags' do expect(described_class).to receive(:enabled?).and_return(true) + allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return('cid') described_class.context(nil) expect(Raven.tags_context[:locale].to_s).to eq(I18n.locale.to_s) + expect(Raven.tags_context[Labkit::Correlation::CorrelationId::LOG_KEY.to_sym].to_s) + .to eq('cid') end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 4e910e67ac2..e44463dd767 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -35,7 +35,7 @@ describe Gitlab::UsageData do subject { described_class.data } it "gathers usage data" do - expect(subject.keys).to match_array(%i( + expect(subject.keys).to include(*%i( active_user_count counts recorded_at @@ -63,12 +63,7 @@ describe Gitlab::UsageData do end it "gathers usage counts" do - count_data = subject[:counts] - - expect(count_data[:boards]).to eq(1) - expect(count_data[:projects]).to eq(3) - - expect(count_data.keys).to match_array(%i( + expected_keys = %i( assignee_lists boards ci_builds @@ -112,6 +107,7 @@ describe Gitlab::UsageData do milestone_lists milestones notes + pool_repositories projects projects_imported_from_github projects_jira_active @@ -132,7 +128,14 @@ describe Gitlab::UsageData do uploads web_hooks user_preferences - )) + ) + + count_data = subject[:counts] + + expect(count_data[:boards]).to eq(1) + expect(count_data[:projects]).to eq(3) + expect(count_data.keys).to include(*expected_keys) + expect(expected_keys - count_data.keys).to be_empty end it 'does not gather user preferences usage data when the feature is disabled' do @@ -211,7 +214,7 @@ describe Gitlab::UsageData do it "gathers license data" do expect(subject[:uuid]).to eq(Gitlab::CurrentSettings.uuid) expect(subject[:version]).to eq(Gitlab::VERSION) - expect(subject[:installation_type]).to eq(Gitlab::INSTALLATION_TYPE) + expect(subject[:installation_type]).to eq('gitlab-development-kit') expect(subject[:active_user_count]).to eq(User.active.count) expect(subject[:recorded_at]).to be_a(Time) end diff --git a/spec/models/active_session_spec.rb b/spec/models/active_session_spec.rb index b523f393ece..2762eaeccd3 100644 --- a/spec/models/active_session_spec.rb +++ b/spec/models/active_session_spec.rb @@ -88,6 +88,52 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do end end + describe '.list_sessions' do + it 'uses the ActiveSession lookup to return original sessions' do + Gitlab::Redis::SharedState.with do |redis| + redis.set("session:gitlab:6919a6f1bb119dd7396fadc38fd18d0d", Marshal.dump({ _csrf_token: 'abcd' })) + + redis.sadd( + "session:lookup:user:gitlab:#{user.id}", + %w[ + 6919a6f1bb119dd7396fadc38fd18d0d + 59822c7d9fcdfa03725eff41782ad97d + ] + ) + end + + expect(ActiveSession.list_sessions(user)).to eq [{ _csrf_token: 'abcd' }] + end + end + + describe '.session_ids_for_user' do + it 'uses the user lookup table to return session ids' do + session_ids = ['59822c7d9fcdfa03725eff41782ad97d'] + + Gitlab::Redis::SharedState.with do |redis| + redis.sadd("session:lookup:user:gitlab:#{user.id}", session_ids) + end + + expect(ActiveSession.session_ids_for_user(user)).to eq(session_ids) + end + end + + describe '.sessions_from_ids' do + it 'uses the ActiveSession lookup to return original sessions' do + Gitlab::Redis::SharedState.with do |redis| + redis.set("session:gitlab:6919a6f1bb119dd7396fadc38fd18d0d", Marshal.dump({ _csrf_token: 'abcd' })) + end + + expect(ActiveSession.sessions_from_ids(['6919a6f1bb119dd7396fadc38fd18d0d'])).to eq [{ _csrf_token: 'abcd' }] + end + + it 'avoids a redis lookup for an empty array' do + expect(Gitlab::Redis::SharedState).not_to receive(:with) + + expect(ActiveSession.sessions_from_ids([])).to eq([]) + end + end + describe '.set' do it 'sets a new redis entry for the user session and a lookup entry' do ActiveSession.set(user, request) diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb index cc90a998d3f..74573d0941c 100644 --- a/spec/models/application_record_spec.rb +++ b/spec/models/application_record_spec.rb @@ -52,4 +52,10 @@ describe ApplicationRecord do expect { Suggestion.find_or_create_by!(note: nil) }.to raise_error(ActiveRecord::RecordInvalid) end end + + describe '.underscore' do + it 'returns the underscored value of the class as a string' do + expect(MergeRequest.underscore).to eq('merge_request') + end + end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 9b489baf163..5f2e8aa0baa 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -2925,26 +2925,18 @@ describe Ci::Build do subject { build.any_unmet_prerequisites? } + before do + allow(build).to receive(:prerequisites).and_return(prerequisites) + end + context 'build has prerequisites' do - before do - allow(build).to receive(:prerequisites).and_return([double]) - end + let(:prerequisites) { [double] } it { is_expected.to be_truthy } - - context 'and the ci_preparing_state feature is disabled' do - before do - stub_feature_flags(ci_preparing_state: false) - end - - it { is_expected.to be_falsey } - end end context 'build does not have prerequisites' do - before do - allow(build).to receive(:prerequisites).and_return([]) - end + let(:prerequisites) { [] } it { is_expected.to be_falsey } end diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb index 81913f4a3b6..1bfc14d2839 100644 --- a/spec/models/ci/pipeline_schedule_spec.rb +++ b/spec/models/ci/pipeline_schedule_spec.rb @@ -35,6 +35,15 @@ describe Ci::PipelineSchedule do expect(pipeline_schedule).not_to be_valid end end + + context 'when cron contains trailing whitespaces' do + it 'strips the attribute' do + pipeline_schedule = build(:ci_pipeline_schedule, cron: ' 0 0 * * * ') + + expect(pipeline_schedule).to be_valid + expect(pipeline_schedule.cron).to eq('0 0 * * *') + end + end end describe '#set_next_run_at' do diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb index bdc0cb8ed86..4f0cd0efe9c 100644 --- a/spec/models/clusters/applications/runner_spec.rb +++ b/spec/models/clusters/applications/runner_spec.rb @@ -69,8 +69,8 @@ describe Clusters::Applications::Runner do expect(values).to include('privileged: true') expect(values).to include('image: ubuntu:16.04') expect(values).to include('resources') - expect(values).to match(/runnerToken: '?#{ci_runner.token}/) - expect(values).to match(/gitlabUrl: '?#{Gitlab::Routing.url_helpers.root_url}/) + expect(values).to match(/runnerToken: '?#{Regexp.escape(ci_runner.token)}/) + expect(values).to match(/gitlabUrl: '?#{Regexp.escape(Gitlab::Routing.url_helpers.root_url)}/) end context 'without a runner' do @@ -83,7 +83,7 @@ describe Clusters::Applications::Runner do end it 'uses the new runner token' do - expect(values).to match(/runnerToken: '?#{runner.token}/) + expect(values).to match(/runnerToken: '?#{Regexp.escape(runner.token)}/) end end @@ -114,6 +114,18 @@ describe Clusters::Applications::Runner do expect(runner.groups).to eq [group] end end + + context 'instance cluster' do + let(:cluster) { create(:cluster, :with_installed_helm, :instance) } + + include_examples 'runner creation' + + it 'creates an instance runner' do + subject + + expect(runner).to be_instance_type + end + end end context 'with duplicated values on vendor/runner/values.yaml' do diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index e1506c06044..58203da5b22 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -325,6 +325,15 @@ describe Clusters::Cluster do end end + context 'when group and instance have configured kubernetes clusters' do + let(:project) { create(:project, group: group) } + let!(:instance_cluster) { create(:cluster, :provided_by_gcp, :instance) } + + it 'returns clusters in order, descending the hierachy' do + is_expected.to eq([group_cluster, instance_cluster]) + end + end + context 'when sub-group has configured kubernetes cluster', :nested_groups do let(:sub_group_cluster) { create(:cluster, :provided_by_gcp, :group) } let(:sub_group) { sub_group_cluster.group } diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index cfe7c7ef0b0..17246f238e0 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -592,9 +592,7 @@ describe Environment do shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do it 'returns the terminals from the deployment service' do - deployment_platform_target = Gitlab.ee? ? environment : project - - expect(deployment_platform_target.deployment_platform) + expect(environment.deployment_platform) .to receive(:terminals).with(environment) .and_return(:fake_terminals) diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index e91b5c4c86f..62663c247d1 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -88,7 +88,7 @@ describe Event do let(:event) { create_push_event(project, user) } it do - expect(event.push?).to be_truthy + expect(event.push_action?).to be_truthy expect(event.visible_to_user?(user)).to be_truthy expect(event.visible_to_user?(nil)).to be_falsey expect(event.tag?).to be_falsey diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 0ce4add5669..cc777cbf749 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -56,14 +56,25 @@ describe Issue do end describe 'locking' do - it 'works when an issue has a NULL lock_version' do - issue = create(:issue) + using RSpec::Parameterized::TableSyntax - described_class.where(id: issue.id).update_all('lock_version = NULL') + where(:lock_version) do + [ + [0], + ["0"] + ] + end - issue.update!(lock_version: 0, title: 'locking test') + with_them do + it 'works when an issue has a NULL lock_version' do + issue = create(:issue) - expect(issue.reload.title).to eq('locking test') + described_class.where(id: issue.id).update_all('lock_version = NULL') + + issue.update!(lock_version: lock_version, title: 'locking test') + + expect(issue.reload.title).to eq('locking test') + end end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index ec2aef6f815..c72b6e9033d 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -32,14 +32,25 @@ describe MergeRequest do end describe 'locking' do - it 'works when a merge request has a NULL lock_version' do - merge_request = create(:merge_request) + using RSpec::Parameterized::TableSyntax - described_class.where(id: merge_request.id).update_all('lock_version = NULL') + where(:lock_version) do + [ + [0], + ["0"] + ] + end - merge_request.update!(lock_version: 0, title: 'locking test') + with_them do + it 'works when a merge request has a NULL lock_version' do + merge_request = create(:merge_request) - expect(merge_request.reload.title).to eq('locking test') + described_class.where(id: merge_request.id).update_all('lock_version = NULL') + + merge_request.update!(lock_version: lock_version, title: 'locking test') + + expect(merge_request.reload.title).to eq('locking test') + end end end diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 4a7eee1fbf3..04ae9390436 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -166,6 +166,13 @@ describe JiraService do ).once end + it 'does not fail if remote_link.all on issue returns nil' do + allow(JIRA::Resource::Remotelink).to receive(:all).and_return(nil) + + expect { @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project)) } + .not_to raise_error(NoMethodError) + end + # Check https://developer.atlassian.com/jiradev/jira-platform/guides/other/guide-jira-remote-issue-links/fields-in-remote-issue-links # for more information it 'creates Remote Link reference in JIRA for comment' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index bb0257e7456..425096d7e80 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -214,6 +214,13 @@ describe Project do expect(project2).not_to be_valid end + it 'validates the visibility' do + expect_any_instance_of(described_class).to receive(:visibility_level_allowed_as_fork).and_call_original + expect_any_instance_of(described_class).to receive(:visibility_level_allowed_by_group).and_call_original + + create(:project) + end + describe 'wiki path conflict' do context "when the new path has been used by the wiki of other Project" do it 'has an error on the name attribute' do @@ -3164,61 +3171,105 @@ describe Project do end describe '.with_feature_available_for_user' do - let!(:user) { create(:user) } - let!(:feature) { MergeRequest } - let!(:project) { create(:project, :public, :merge_requests_enabled) } + let(:user) { create(:user) } + let(:feature) { MergeRequest } subject { described_class.with_feature_available_for_user(feature, user) } - context 'when user has access to project' do - subject { described_class.with_feature_available_for_user(feature, user) } + shared_examples 'feature disabled' do + let(:project) { create(:project, :public, :merge_requests_disabled) } + + it 'does not return projects with the project feature disabled' do + is_expected.not_to include(project) + end + end + + shared_examples 'feature public' do + let(:project) { create(:project, :public, :merge_requests_public) } + + it 'returns projects with the project feature public' do + is_expected.to include(project) + end + end + + shared_examples 'feature enabled' do + let(:project) { create(:project, :public, :merge_requests_enabled) } + + it 'returns projects with the project feature enabled' do + is_expected.to include(project) + end + end + + shared_examples 'feature access level is nil' do + let(:project) { create(:project, :public) } + + it 'returns projects with the project feature access level nil' do + project.project_feature.update(merge_requests_access_level: nil) + is_expected.to include(project) + end + end + + context 'with user' do before do project.add_guest(user) end - context 'when public project' do - context 'when feature is public' do - it 'returns project' do - is_expected.to include(project) + it_behaves_like 'feature disabled' + it_behaves_like 'feature public' + it_behaves_like 'feature enabled' + it_behaves_like 'feature access level is nil' + + context 'when feature is private' do + let(:project) { create(:project, :public, :merge_requests_private) } + + context 'when user does not has access to the feature' do + it 'does not return projects with the project feature private' do + is_expected.not_to include(project) end end - context 'when feature is private' do - let!(:project) { create(:project, :public, :merge_requests_private) } - - it 'returns project when user has access to the feature' do - project.add_maintainer(user) + context 'when user has access to the feature' do + it 'returns projects with the project feature private' do + project.add_reporter(user) is_expected.to include(project) end - - it 'does not return project when user does not have the minimum access level required' do - is_expected.not_to include(project) - end end end + end - context 'when private project' do - let!(:project) { create(:project) } + context 'user is an admin' do + let(:user) { create(:user, :admin) } - it 'returns project when user has access to the feature' do - project.add_maintainer(user) + it_behaves_like 'feature disabled' + it_behaves_like 'feature public' + it_behaves_like 'feature enabled' + it_behaves_like 'feature access level is nil' - is_expected.to include(project) - end + context 'when feature is private' do + let(:project) { create(:project, :public, :merge_requests_private) } - it 'does not return project when user does not have the minimum access level required' do - is_expected.not_to include(project) + it 'returns projects with the project feature private' do + is_expected.to include(project) end end end - context 'when user does not have access to project' do - let!(:project) { create(:project) } + context 'without user' do + let(:user) { nil } - it 'does not return project when user cant access project' do - is_expected.not_to include(project) + it_behaves_like 'feature disabled' + it_behaves_like 'feature public' + it_behaves_like 'feature enabled' + it_behaves_like 'feature access level is nil' + + context 'when feature is private' do + let(:project) { create(:project, :public, :merge_requests_private) } + + it 'does not return projects with the project feature private' do + is_expected.not_to include(project) + end end end end diff --git a/spec/models/push_event_spec.rb b/spec/models/push_event_spec.rb index f86500f91cd..5509ed87308 100644 --- a/spec/models/push_event_spec.rb +++ b/spec/models/push_event_spec.rb @@ -123,9 +123,9 @@ describe PushEvent do end end - describe '#push?' do + describe '#push_action?' do it 'returns true' do - expect(event).to be_push + expect(event).to be_push_action end end diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb index f743dfed31f..e14b19db915 100644 --- a/spec/models/remote_mirror_spec.rb +++ b/spec/models/remote_mirror_spec.rb @@ -373,6 +373,22 @@ describe RemoteMirror, :mailer do end end + describe '#disabled?' do + subject { remote_mirror.disabled? } + + context 'when disabled' do + let(:remote_mirror) { build(:remote_mirror, enabled: false) } + + it { is_expected.to be_truthy } + end + + context 'when enabled' do + let(:remote_mirror) { build(:remote_mirror, enabled: true) } + + it { is_expected.to be_falsy } + end + end + def create_mirror(params) project = FactoryBot.create(:project, :repository) project.remote_mirrors.create!(params) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 9ff0f355fd4..c5ab7e57272 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -2286,12 +2286,45 @@ describe Repository do end describe '#diverging_commit_counts' do + let(:diverged_branch) { repository.find_branch('fix') } + let(:root_ref_sha) { repository.raw_repository.commit(repository.root_ref).id } + let(:diverged_branch_sha) { diverged_branch.dereferenced_target.sha } + it 'returns the commit counts behind and ahead of default branch' do - result = repository.diverging_commit_counts( - repository.find_branch('fix')) + result = repository.diverging_commit_counts(diverged_branch) expect(result).to eq(behind: 29, ahead: 2) end + + context 'when gitaly_count_diverging_commits_no_max is enabled' do + before do + stub_feature_flags(gitaly_count_diverging_commits_no_max: true) + end + + it 'calls diverging_commit_count without max count' do + expect(repository.raw_repository) + .to receive(:diverging_commit_count) + .with(root_ref_sha, diverged_branch_sha) + .and_return([29, 2]) + + repository.diverging_commit_counts(diverged_branch) + end + end + + context 'when gitaly_count_diverging_commits_no_max is disabled' do + before do + stub_feature_flags(gitaly_count_diverging_commits_no_max: false) + end + + it 'calls diverging_commit_count with max count' do + expect(repository.raw_repository) + .to receive(:diverging_commit_count) + .with(root_ref_sha, diverged_branch_sha, max_count: Repository::MAX_DIVERGING_COUNT) + .and_return([29, 2]) + + repository.diverging_commit_counts(diverged_branch) + end + end end describe '#refresh_method_caches' do diff --git a/spec/models/user_preference_spec.rb b/spec/models/user_preference_spec.rb index b2ef17a81d4..e09c91e874a 100644 --- a/spec/models/user_preference_spec.rb +++ b/spec/models/user_preference_spec.rb @@ -73,4 +73,10 @@ describe UserPreference do it_behaves_like 'a sort_by preference' end end + + describe '#timezone' do + it 'returns server time as default' do + expect(user_preference.timezone).to eq(Time.zone.tzinfo.name) + end + end end diff --git a/spec/policies/clusters/cluster_policy_spec.rb b/spec/policies/clusters/cluster_policy_spec.rb index b2f0ca1bc30..cc3dde154dc 100644 --- a/spec/policies/clusters/cluster_policy_spec.rb +++ b/spec/policies/clusters/cluster_policy_spec.rb @@ -66,5 +66,21 @@ describe Clusters::ClusterPolicy, :models do it { expect(policy).to be_disallowed :admin_cluster } end end + + context 'instance cluster' do + let(:cluster) { create(:cluster, :instance) } + + context 'when user' do + it { expect(policy).to be_disallowed :update_cluster } + it { expect(policy).to be_disallowed :admin_cluster } + end + + context 'when admin' do + let(:user) { create(:admin) } + + it { expect(policy).to be_allowed :update_cluster } + it { expect(policy).to be_allowed :admin_cluster } + end + end end end diff --git a/spec/policies/clusters/instance_policy_spec.rb b/spec/policies/clusters/instance_policy_spec.rb new file mode 100644 index 00000000000..9d755c6d29d --- /dev/null +++ b/spec/policies/clusters/instance_policy_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Clusters::InstancePolicy do + let(:user) { create(:user) } + let(:policy) { described_class.new(user, Clusters::Instance.new) } + + describe 'rules' do + context 'when user' do + it { expect(policy).to be_disallowed :read_cluster } + it { expect(policy).to be_disallowed :update_cluster } + it { expect(policy).to be_disallowed :admin_cluster } + end + + context 'when admin' do + let(:user) { create(:admin) } + + context 'with instance_level_clusters enabled' do + it { expect(policy).to be_allowed :read_cluster } + it { expect(policy).to be_allowed :update_cluster } + it { expect(policy).to be_allowed :admin_cluster } + end + + context 'with instance_level_clusters disabled' do + before do + stub_feature_flags(instance_clusters: false) + end + + it { expect(policy).to be_disallowed :read_cluster } + it { expect(policy).to be_disallowed :update_cluster } + it { expect(policy).to be_disallowed :admin_cluster } + end + end + end +end diff --git a/spec/presenters/clusters/cluster_presenter_spec.rb b/spec/presenters/clusters/cluster_presenter_spec.rb index a9d786bc872..42701a5f8d1 100644 --- a/spec/presenters/clusters/cluster_presenter_spec.rb +++ b/spec/presenters/clusters/cluster_presenter_spec.rb @@ -210,6 +210,12 @@ describe Clusters::ClusterPresenter do it { is_expected.to eq('Group cluster') } end + + context 'instance_type cluster' do + let(:cluster) { create(:cluster, :provided_by_gcp, :instance) } + + it { is_expected.to eq('Instance cluster') } + end end describe '#show_path' do @@ -227,6 +233,12 @@ describe Clusters::ClusterPresenter do it { is_expected.to eq(group_cluster_path(group, cluster)) } end + + context 'instance_type cluster' do + let(:cluster) { create(:cluster, :provided_by_gcp, :instance) } + + it { is_expected.to eq(admin_cluster_path(cluster)) } + end end describe '#read_only_kubernetes_platform_fields?' do diff --git a/spec/presenters/group_clusterable_presenter_spec.rb b/spec/presenters/group_clusterable_presenter_spec.rb index cb623fa1fa4..fa77273f6aa 100644 --- a/spec/presenters/group_clusterable_presenter_spec.rb +++ b/spec/presenters/group_clusterable_presenter_spec.rb @@ -82,10 +82,4 @@ describe GroupClusterablePresenter do it { is_expected.to eq(group_cluster_path(group, cluster)) } end - - describe '#clusters_path' do - subject { presenter.clusters_path } - - it { is_expected.to eq(group_clusters_path(group)) } - end end diff --git a/spec/presenters/project_clusterable_presenter_spec.rb b/spec/presenters/project_clusterable_presenter_spec.rb index e5857f75aed..6786a84243f 100644 --- a/spec/presenters/project_clusterable_presenter_spec.rb +++ b/spec/presenters/project_clusterable_presenter_spec.rb @@ -82,10 +82,4 @@ describe ProjectClusterablePresenter do it { is_expected.to eq(project_cluster_path(project, cluster)) } end - - describe '#clusters_path' do - subject { presenter.clusters_path } - - it { is_expected.to eq(project_clusters_path(project)) } - end end diff --git a/spec/requests/api/discussions_spec.rb b/spec/requests/api/discussions_spec.rb index 35c448d187d..ca1ffe3c524 100644 --- a/spec/requests/api/discussions_spec.rb +++ b/spec/requests/api/discussions_spec.rb @@ -13,7 +13,7 @@ describe API::Discussions do let!(:issue) { create(:issue, project: project, author: user) } let!(:issue_note) { create(:discussion_note_on_issue, noteable: issue, project: project, author: user) } - it_behaves_like 'discussions API', 'projects', 'issues', 'iid' do + it_behaves_like 'discussions API', 'projects', 'issues', 'iid', can_reply_to_individual_notes: true do let(:parent) { project } let(:noteable) { issue } let(:note) { issue_note } @@ -37,7 +37,7 @@ describe API::Discussions do let!(:diff_note) { create(:diff_note_on_merge_request, noteable: noteable, project: project, author: user) } let(:parent) { project } - it_behaves_like 'discussions API', 'projects', 'merge_requests', 'iid' + it_behaves_like 'discussions API', 'projects', 'merge_requests', 'iid', can_reply_to_individual_notes: true it_behaves_like 'diff discussions API', 'projects', 'merge_requests', 'iid' it_behaves_like 'resolvable discussions API', 'projects', 'merge_requests', 'iid' end diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb index b63b4fb34df..dd518274f82 100644 --- a/spec/requests/api/graphql/gitlab_schema_spec.rb +++ b/spec/requests/api/graphql/gitlab_schema_spec.rb @@ -3,15 +3,43 @@ require 'spec_helper' describe 'GitlabSchema configurations' do include GraphqlHelpers - it 'shows an error if complexity is too high' do - project = create(:project, :repository) - query = graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id name description)) + let(:project) { create(:project, :repository) } + let(:query) { graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id name description)) } + let(:current_user) { create(:user) } - allow(GitlabSchema).to receive(:max_query_complexity).and_return 1 + describe '#max_complexity' do + context 'when complexity is too high' do + it 'shows an error' do + allow(GitlabSchema).to receive(:max_query_complexity).and_return 1 - post_graphql(query, current_user: nil) + post_graphql(query, current_user: nil) - expect(graphql_errors.first['message']).to include('which exceeds max complexity of 1') + expect(graphql_errors.first['message']).to include('which exceeds max complexity of 1') + end + end + end + + describe '#max_depth' do + context 'when query depth is too high' do + it 'shows error' do + errors = [{ "message" => "Query has depth of 2, which exceeds max depth of 1" }] + allow(GitlabSchema).to receive(:max_query_depth).and_return 1 + + post_graphql(query) + + expect(graphql_errors).to eq(errors) + end + end + + context 'when query depth is within range' do + it 'has no error' do + allow(GitlabSchema).to receive(:max_query_depth).and_return 5 + + post_graphql(query) + + expect(graphql_errors).to be_nil + end + end end context 'when IntrospectionQuery' do diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index 25a312cb734..ed907841bd8 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -247,9 +247,8 @@ describe API::Helpers do exception = RuntimeError.new('test error') allow(exception).to receive(:backtrace).and_return(caller) - expect(Raven).to receive(:capture_exception).with(exception, tags: { - correlation_id: 'new-correlation-id' - }, extra: {}) + expect(Raven).to receive(:capture_exception).with(exception, tags: + a_hash_including(correlation_id: 'new-correlation-id'), extra: {}) Labkit::Correlation::CorrelationId.use_id('new-correlation-id') do handle_api_exception(exception) diff --git a/spec/requests/api/issues/get_group_issues_spec.rb b/spec/requests/api/issues/get_group_issues_spec.rb new file mode 100644 index 00000000000..8b02cf56e9f --- /dev/null +++ b/spec/requests/api/issues/get_group_issues_spec.rb @@ -0,0 +1,652 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::Issues do + set(:user) { create(:user) } + let(:user2) { create(:user) } + let(:non_member) { create(:user) } + set(:guest) { create(:user) } + set(:author) { create(:author) } + set(:assignee) { create(:assignee) } + let(:admin) { create(:user, :admin) } + + let(:issue_title) { 'foo' } + let(:issue_description) { 'closed' } + + let(:no_milestone_title) { 'None' } + let(:any_milestone_title) { 'Any' } + + before do + stub_licensed_features(multiple_issue_assignees: false, issue_weights: false) + end + + describe 'GET /groups/:id/issues' do + let!(:group) { create(:group) } + let!(:group_project) { create(:project, :public, creator_id: user.id, namespace: group) } + let!(:group_closed_issue) do + create :closed_issue, + author: user, + assignees: [user], + project: group_project, + state: :closed, + milestone: group_milestone, + updated_at: 3.hours.ago, + created_at: 1.day.ago + end + let!(:group_confidential_issue) do + create :issue, + :confidential, + project: group_project, + author: author, + assignees: [assignee], + updated_at: 2.hours.ago, + created_at: 2.days.ago + end + let!(:group_issue) do + create :issue, + author: user, + assignees: [user], + project: group_project, + milestone: group_milestone, + updated_at: 1.hour.ago, + title: issue_title, + description: issue_description, + created_at: 5.days.ago + end + let!(:group_label) do + create(:label, title: 'group_lbl', color: '#FFAABB', project: group_project) + end + let!(:group_label_link) { create(:label_link, label: group_label, target: group_issue) } + let!(:group_milestone) { create(:milestone, title: '3.0.0', project: group_project) } + let!(:group_empty_milestone) do + create(:milestone, title: '4.0.0', project: group_project) + end + let!(:group_note) { create(:note_on_issue, author: user, project: group_project, noteable: group_issue) } + + let(:base_url) { "/groups/#{group.id}/issues" } + + shared_examples 'group issues statistics' do + it 'returns issues statistics' do + get api("/groups/#{group.id}/issues_statistics", user), params: params + + expect(response).to have_gitlab_http_status(200) + expect(json_response['statistics']).not_to be_nil + expect(json_response['statistics']['counts']['all']).to eq counts[:all] + expect(json_response['statistics']['counts']['closed']).to eq counts[:closed] + expect(json_response['statistics']['counts']['opened']).to eq counts[:opened] + end + end + + context 'when group has subgroups', :nested_groups do + let(:subgroup_1) { create(:group, parent: group) } + let(:subgroup_2) { create(:group, parent: subgroup_1) } + + let(:subgroup_1_project) { create(:project, :public, namespace: subgroup_1) } + let(:subgroup_2_project) { create(:project, namespace: subgroup_2) } + + let!(:issue_1) { create(:issue, project: subgroup_1_project) } + let!(:issue_2) { create(:issue, project: subgroup_2_project) } + + context 'when user is unauthenticated' do + it 'also returns subgroups public projects issues' do + get api(base_url) + + expect_paginated_array_response([issue_1.id, group_closed_issue.id, group_issue.id]) + end + + it 'also returns subgroups public projects issues filtered by milestone' do + get api(base_url), params: { milestone: group_milestone.title } + + expect_paginated_array_response([group_closed_issue.id, group_issue.id]) + end + + context 'issues_statistics' do + context 'no state is treated as all state' do + let(:params) { {} } + let(:counts) { { all: 3, closed: 1, opened: 2 } } + + it_behaves_like 'group issues statistics' + end + + context 'statistics when all state is passed' do + let(:params) { { state: :all } } + let(:counts) { { all: 3, closed: 1, opened: 2 } } + + it_behaves_like 'group issues statistics' + end + + context 'closed state is treated as all state' do + let(:params) { { state: :closed } } + let(:counts) { { all: 3, closed: 1, opened: 2 } } + + it_behaves_like 'group issues statistics' + end + + context 'opened state is treated as all state' do + let(:params) { { state: :opened } } + let(:counts) { { all: 3, closed: 1, opened: 2 } } + + it_behaves_like 'group issues statistics' + end + + context 'when filtering by milestone and no state treated as all state' do + let(:params) { { milestone: group_milestone.title } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'group issues statistics' + end + + context 'when filtering by milestone and all state' do + let(:params) { { milestone: group_milestone.title, state: :all } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'group issues statistics' + end + + context 'when filtering by milestone and closed state treated as all state' do + let(:params) { { milestone: group_milestone.title, state: :closed } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'group issues statistics' + end + + context 'when filtering by milestone and opened state treated as all state' do + let(:params) { { milestone: group_milestone.title, state: :opened } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'group issues statistics' + end + end + end + + context 'when user is a group member' do + before do + group.add_developer(user) + end + + it 'also returns subgroups projects issues' do + get api(base_url, user) + + expect_paginated_array_response([issue_2.id, issue_1.id, group_closed_issue.id, group_confidential_issue.id, group_issue.id]) + end + + it 'also returns subgroups public projects issues filtered by milestone' do + get api(base_url, user), params: { milestone: group_milestone.title } + + expect_paginated_array_response([group_closed_issue.id, group_issue.id]) + end + + context 'issues_statistics' do + context 'no state is treated as all state' do + let(:params) { {} } + let(:counts) { { all: 5, closed: 1, opened: 4 } } + + it_behaves_like 'group issues statistics' + end + + context 'statistics when all state is passed' do + let(:params) { { state: :all } } + let(:counts) { { all: 5, closed: 1, opened: 4 } } + + it_behaves_like 'group issues statistics' + end + + context 'closed state is treated as all state' do + let(:params) { { state: :closed } } + let(:counts) { { all: 5, closed: 1, opened: 4 } } + + it_behaves_like 'group issues statistics' + end + + context 'opened state is treated as all state' do + let(:params) { { state: :opened } } + let(:counts) { { all: 5, closed: 1, opened: 4 } } + + it_behaves_like 'group issues statistics' + end + + context 'when filtering by milestone and no state treated as all state' do + let(:params) { { milestone: group_milestone.title } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'group issues statistics' + end + + context 'when filtering by milestone and all state' do + let(:params) { { milestone: group_milestone.title, state: :all } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'group issues statistics' + end + + context 'when filtering by milestone and closed state treated as all state' do + let(:params) { { milestone: group_milestone.title, state: :closed } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'group issues statistics' + end + + context 'when filtering by milestone and opened state treated as all state' do + let(:params) { { milestone: group_milestone.title, state: :opened } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'group issues statistics' + end + end + end + end + + context 'when user is unauthenticated' do + it 'lists all issues in public projects' do + get api(base_url) + + expect_paginated_array_response([group_closed_issue.id, group_issue.id]) + end + + it 'also returns subgroups public projects issues filtered by milestone' do + get api(base_url), params: { milestone: group_milestone.title } + + expect_paginated_array_response([group_closed_issue.id, group_issue.id]) + end + + context 'issues_statistics' do + context 'no state is treated as all state' do + let(:params) { {} } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'group issues statistics' + end + + context 'statistics when all state is passed' do + let(:params) { { state: :all } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'group issues statistics' + end + + context 'closed state is treated as all state' do + let(:params) { { state: :closed } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'group issues statistics' + end + + context 'opened state is treated as all state' do + let(:params) { { state: :opened } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'group issues statistics' + end + + context 'when filtering by milestone and no state treated as all state' do + let(:params) { { milestone: group_milestone.title } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'group issues statistics' + end + + context 'when filtering by milestone and all state' do + let(:params) { { milestone: group_milestone.title, state: :all } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'group issues statistics' + end + + context 'when filtering by milestone and closed state treated as all state' do + let(:params) { { milestone: group_milestone.title, state: :closed } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'group issues statistics' + end + + context 'when filtering by milestone and opened state treated as all state' do + let(:params) { { milestone: group_milestone.title, state: :opened } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'group issues statistics' + end + end + end + + context 'when user is a group member' do + before do + group_project.add_reporter(user) + end + + it 'returns all group issues (including opened and closed)' do + get api(base_url, admin) + + expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id]) + end + + it 'returns group issues without confidential issues for non project members' do + get api(base_url, non_member), params: { state: :opened } + + expect_paginated_array_response(group_issue.id) + end + + it 'returns group confidential issues for author' do + get api(base_url, author), params: { state: :opened } + + expect_paginated_array_response([group_confidential_issue.id, group_issue.id]) + end + + it 'returns group confidential issues for assignee' do + get api(base_url, assignee), params: { state: :opened } + + expect_paginated_array_response([group_confidential_issue.id, group_issue.id]) + end + + it 'returns group issues with confidential issues for project members' do + get api(base_url, user), params: { state: :opened } + + expect_paginated_array_response([group_confidential_issue.id, group_issue.id]) + end + + it 'returns group confidential issues for admin' do + get api(base_url, admin), params: { state: :opened } + + expect_paginated_array_response([group_confidential_issue.id, group_issue.id]) + end + + it 'returns only confidential issues' do + get api(base_url, user), params: { confidential: true } + + expect_paginated_array_response(group_confidential_issue.id) + end + + it 'returns only public issues' do + get api(base_url, user), params: { confidential: false } + + expect_paginated_array_response([group_closed_issue.id, group_issue.id]) + end + + it 'returns an array of labeled group issues' do + get api(base_url, user), params: { labels: group_label.title } + + expect_paginated_array_response(group_issue.id) + expect(json_response.first['labels']).to eq([group_label.title]) + end + + it 'returns an array of labeled group issues with labels param as array' do + get api(base_url, user), params: { labels: [group_label.title] } + + expect_paginated_array_response(group_issue.id) + expect(json_response.first['labels']).to eq([group_label.title]) + end + + it 'returns an array of labeled group issues where all labels match' do + get api(base_url, user), params: { labels: "#{group_label.title},foo,bar" } + + expect_paginated_array_response([]) + end + + it 'returns an array of labeled group issues where all labels match with labels param as array' do + get api(base_url, user), params: { labels: [group_label.title, 'foo', 'bar'] } + + expect_paginated_array_response([]) + end + + it 'returns issues matching given search string for title' do + get api(base_url, user), params: { search: group_issue.title } + + expect_paginated_array_response(group_issue.id) + end + + it 'returns issues matching given search string for description' do + get api(base_url, user), params: { search: group_issue.description } + + expect_paginated_array_response(group_issue.id) + end + + context 'with labeled issues' do + let(:label_b) { create(:label, title: 'foo', project: group_project) } + let(:label_c) { create(:label, title: 'bar', project: group_project) } + + before do + create(:label_link, label: label_b, target: group_issue) + create(:label_link, label: label_c, target: group_issue) + + get api(base_url, user), params: params + end + + let(:issue) { group_issue } + let(:label) { group_label } + + it_behaves_like 'labeled issues with labels and label_name params' + end + + it 'returns an array of issues found by iids' do + get api(base_url, user), params: { iids: [group_issue.iid] } + + expect_paginated_array_response(group_issue.id) + expect(json_response.first['id']).to eq(group_issue.id) + end + + it 'returns an empty array if iid does not exist' do + get api(base_url, user), params: { iids: [0] } + + expect_paginated_array_response([]) + end + + it 'returns an empty array if no group issue matches labels' do + get api(base_url, user), params: { labels: 'foo,bar' } + + expect_paginated_array_response([]) + end + + it 'returns an array of group issues with any label' do + get api(base_url, user), params: { labels: IssuesFinder::FILTER_ANY } + + expect_paginated_array_response(group_issue.id) + expect(json_response.first['id']).to eq(group_issue.id) + end + + it 'returns an array of group issues with any label with labels param as array' do + get api(base_url, user), params: { labels: [IssuesFinder::FILTER_ANY] } + + expect_paginated_array_response(group_issue.id) + expect(json_response.first['id']).to eq(group_issue.id) + end + + it 'returns an array of group issues with no label' do + get api(base_url, user), params: { labels: IssuesFinder::FILTER_NONE } + + expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id]) + end + + it 'returns an array of group issues with no label with labels param as array' do + get api(base_url, user), params: { labels: [IssuesFinder::FILTER_NONE] } + + expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id]) + end + + it 'returns an empty array if no issue matches milestone' do + get api(base_url, user), params: { milestone: group_empty_milestone.title } + + expect_paginated_array_response([]) + end + + it 'returns an empty array if milestone does not exist' do + get api(base_url, user), params: { milestone: 'foo' } + + expect_paginated_array_response([]) + end + + it 'returns an array of issues in given milestone' do + get api(base_url, user), params: { state: :opened, milestone: group_milestone.title } + + expect_paginated_array_response(group_issue.id) + end + + it 'returns an array of issues matching state in milestone' do + get api(base_url, user), params: { milestone: group_milestone.title, state: :closed } + + expect_paginated_array_response(group_closed_issue.id) + end + + it 'returns an array of issues with no milestone' do + get api(base_url, user), params: { milestone: no_milestone_title } + + expect(response).to have_gitlab_http_status(200) + + expect_paginated_array_response(group_confidential_issue.id) + end + + context 'without sort params' do + it 'sorts by created_at descending by default' do + get api(base_url, user) + + expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id]) + end + + context 'with 2 issues with same created_at' do + let!(:group_issue2) do + create :issue, + author: user, + assignees: [user], + project: group_project, + milestone: group_milestone, + updated_at: 1.hour.ago, + title: issue_title, + description: issue_description, + created_at: group_issue.created_at + end + + it 'page breaks first page correctly' do + get api("#{base_url}?per_page=3", user) + + expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue2.id]) + end + + it 'page breaks second page correctly' do + get api("#{base_url}?per_page=3&page=2", user) + + expect_paginated_array_response([group_issue.id]) + end + end + end + + it 'sorts ascending when requested' do + get api("#{base_url}?sort=asc", user) + + expect_paginated_array_response([group_issue.id, group_confidential_issue.id, group_closed_issue.id]) + end + + it 'sorts by updated_at descending when requested' do + get api("#{base_url}?order_by=updated_at", user) + + group_issue.touch(:updated_at) + + expect_paginated_array_response([group_issue.id, group_confidential_issue.id, group_closed_issue.id]) + end + + it 'sorts by updated_at ascending when requested' do + get api(base_url, user), params: { order_by: :updated_at, sort: :asc } + + expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id]) + end + + context 'issues_statistics' do + context 'no state is treated as all state' do + let(:params) { {} } + let(:counts) { { all: 3, closed: 1, opened: 2 } } + + it_behaves_like 'group issues statistics' + end + + context 'statistics when all state is passed' do + let(:params) { { state: :all } } + let(:counts) { { all: 3, closed: 1, opened: 2 } } + + it_behaves_like 'group issues statistics' + end + + context 'closed state is treated as all state' do + let(:params) { { state: :closed } } + let(:counts) { { all: 3, closed: 1, opened: 2 } } + + it_behaves_like 'group issues statistics' + end + + context 'opened state is treated as all state' do + let(:params) { { state: :opened } } + let(:counts) { { all: 3, closed: 1, opened: 2 } } + + it_behaves_like 'group issues statistics' + end + + context 'when filtering by milestone and no state treated as all state' do + let(:params) { { milestone: group_milestone.title } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'group issues statistics' + end + + context 'when filtering by milestone and all state' do + let(:params) { { milestone: group_milestone.title, state: :all } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'group issues statistics' + end + + context 'when filtering by milestone and closed state treated as all state' do + let(:params) { { milestone: group_milestone.title, state: :closed } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'group issues statistics' + end + + context 'when filtering by milestone and opened state treated as all state' do + let(:params) { { milestone: group_milestone.title, state: :opened } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'group issues statistics' + end + + context 'sort does not affect statistics ' do + let(:params) { { state: :opened, order_by: 'updated_at' } } + let(:counts) { { all: 3, closed: 1, opened: 2 } } + + it_behaves_like 'group issues statistics' + end + end + + context 'filtering by assignee_username' do + let(:another_assignee) { create(:assignee) } + let!(:issue1) { create(:issue, author: user2, project: group_project, created_at: 3.days.ago) } + let!(:issue2) { create(:issue, author: user2, project: group_project, created_at: 2.days.ago) } + let!(:issue3) { create(:issue, author: user2, assignees: [assignee, another_assignee], project: group_project, created_at: 1.day.ago) } + + it 'returns issues with by assignee_username' do + get api(base_url, user), params: { assignee_username: [assignee.username], scope: 'all' } + + expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id]) + expect_paginated_array_response([issue3.id, group_confidential_issue.id]) + end + + it 'returns issues by assignee_username as string' do + get api(base_url, user), params: { assignee_username: assignee.username, scope: 'all' } + + expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id]) + expect_paginated_array_response([issue3.id, group_confidential_issue.id]) + end + + it 'returns error when multiple assignees are passed' do + get api(base_url, user), params: { assignee_username: [assignee.username, another_assignee.username], scope: 'all' } + + expect(response).to have_gitlab_http_status(400) + expect(json_response["error"]).to include("allows one value, but found 2") + end + + it 'returns error when assignee_username and assignee_id are passed together' do + get api(base_url, user), params: { assignee_username: [assignee.username], assignee_id: another_assignee.id, scope: 'all' } + + expect(response).to have_gitlab_http_status(400) + expect(json_response["error"]).to include("mutually exclusive") + end + end + end + end +end diff --git a/spec/requests/api/issues/get_project_issues_spec.rb b/spec/requests/api/issues/get_project_issues_spec.rb new file mode 100644 index 00000000000..a07d7673345 --- /dev/null +++ b/spec/requests/api/issues/get_project_issues_spec.rb @@ -0,0 +1,805 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::Issues do + set(:user) { create(:user) } + set(:project) do + create(:project, :public, creator_id: user.id, namespace: user.namespace) + end + + let(:user2) { create(:user) } + let(:non_member) { create(:user) } + set(:guest) { create(:user) } + set(:author) { create(:author) } + set(:assignee) { create(:assignee) } + let(:admin) { create(:user, :admin) } + let(:issue_title) { 'foo' } + let(:issue_description) { 'closed' } + let!(:closed_issue) do + create :closed_issue, + author: user, + assignees: [user], + project: project, + state: :closed, + milestone: milestone, + created_at: generate(:past_time), + updated_at: 3.hours.ago, + closed_at: 1.hour.ago + end + let!(:confidential_issue) do + create :issue, + :confidential, + project: project, + author: author, + assignees: [assignee], + created_at: generate(:past_time), + updated_at: 2.hours.ago + end + let!(:issue) do + create :issue, + author: user, + assignees: [user], + project: project, + milestone: milestone, + created_at: generate(:past_time), + updated_at: 1.hour.ago, + title: issue_title, + description: issue_description + end + set(:label) do + create(:label, title: 'label', color: '#FFAABB', project: project) + end + let!(:label_link) { create(:label_link, label: label, target: issue) } + let(:milestone) { create(:milestone, title: '1.0.0', project: project) } + set(:empty_milestone) do + create(:milestone, title: '2.0.0', project: project) + end + let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) } + + let(:no_milestone_title) { 'None' } + let(:any_milestone_title) { 'Any' } + + before(:all) do + project.add_reporter(user) + project.add_guest(guest) + end + + before do + stub_licensed_features(multiple_issue_assignees: false, issue_weights: false) + end + + shared_examples 'project issues statistics' do + it 'returns project issues statistics' do + get api("/issues_statistics", user), params: params + + expect(response).to have_gitlab_http_status(200) + expect(json_response['statistics']).not_to be_nil + expect(json_response['statistics']['counts']['all']).to eq counts[:all] + expect(json_response['statistics']['counts']['closed']).to eq counts[:closed] + expect(json_response['statistics']['counts']['opened']).to eq counts[:opened] + end + end + + describe "GET /projects/:id/issues" do + let(:base_url) { "/projects/#{project.id}" } + + context 'when unauthenticated' do + it 'returns public project issues' do + get api("/projects/#{project.id}/issues") + + expect_paginated_array_response([issue.id, closed_issue.id]) + end + + context 'issues_statistics' do + context 'no state is treated as all state' do + let(:params) { {} } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'project issues statistics' + end + + context 'statistics when all state is passed' do + let(:params) { { state: :all } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'project issues statistics' + end + + context 'closed state is treated as all state' do + let(:params) { { state: :closed } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'project issues statistics' + end + + context 'opened state is treated as all state' do + let(:params) { { state: :opened } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'project issues statistics' + end + + context 'when filtering by milestone and no state treated as all state' do + let(:params) { { milestone: milestone.title } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'project issues statistics' + end + + context 'when filtering by milestone and all state' do + let(:params) { { milestone: milestone.title, state: :all } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'project issues statistics' + end + + context 'when filtering by milestone and closed state treated as all state' do + let(:params) { { milestone: milestone.title, state: :closed } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'project issues statistics' + end + + context 'when filtering by milestone and opened state treated as all state' do + let(:params) { { milestone: milestone.title, state: :opened } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'project issues statistics' + end + + context 'sort does not affect statistics ' do + let(:params) { { state: :opened, order_by: 'updated_at' } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'project issues statistics' + end + end + end + + it 'avoids N+1 queries' do + get api("/projects/#{project.id}/issues", user) + + create_list(:issue, 3, project: project) + + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do + get api("/projects/#{project.id}/issues", user) + end.count + + expect do + get api("/projects/#{project.id}/issues", user) + end.not_to exceed_all_query_limit(control_count) + end + + it 'returns 404 when project does not exist' do + get api('/projects/1000/issues', non_member) + + expect(response).to have_gitlab_http_status(404) + end + + it 'returns 404 on private projects for other users' do + private_project = create(:project, :private) + create(:issue, project: private_project) + + get api("/projects/#{private_project.id}/issues", non_member) + + expect(response).to have_gitlab_http_status(404) + end + + it 'returns no issues when user has access to project but not issues' do + restricted_project = create(:project, :public, :issues_private) + create(:issue, project: restricted_project) + + get api("/projects/#{restricted_project.id}/issues", non_member) + + expect_paginated_array_response([]) + end + + it 'returns project issues without confidential issues for non project members' do + get api("#{base_url}/issues", non_member) + + expect_paginated_array_response([issue.id, closed_issue.id]) + end + + it 'returns project issues without confidential issues for project members with guest role' do + get api("#{base_url}/issues", guest) + + expect_paginated_array_response([issue.id, closed_issue.id]) + end + + it 'returns project confidential issues for author' do + get api("#{base_url}/issues", author) + + expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id]) + end + + it 'returns only confidential issues' do + get api("#{base_url}/issues", author), params: { confidential: true } + + expect_paginated_array_response(confidential_issue.id) + end + + it 'returns only public issues' do + get api("#{base_url}/issues", author), params: { confidential: false } + + expect_paginated_array_response([issue.id, closed_issue.id]) + end + + it 'returns project confidential issues for assignee' do + get api("#{base_url}/issues", assignee) + + expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id]) + end + + it 'returns project issues with confidential issues for project members' do + get api("#{base_url}/issues", user) + + expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id]) + end + + it 'returns project confidential issues for admin' do + get api("#{base_url}/issues", admin) + + expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id]) + end + + it 'returns an array of labeled project issues' do + get api("#{base_url}/issues", user), params: { labels: label.title } + + expect_paginated_array_response(issue.id) + end + + it 'returns an array of labeled project issues with labels param as array' do + get api("#{base_url}/issues", user), params: { labels: [label.title] } + + expect_paginated_array_response(issue.id) + end + + context 'with labeled issues' do + let(:label_b) { create(:label, title: 'foo', project: project) } + let(:label_c) { create(:label, title: 'bar', project: project) } + + before do + create(:label_link, label: label_b, target: issue) + create(:label_link, label: label_c, target: issue) + + get api('/issues', user), params: params + end + + it_behaves_like 'labeled issues with labels and label_name params' + end + + it 'returns issues matching given search string for title' do + get api("#{base_url}/issues?search=#{issue.title}", user) + + expect_paginated_array_response(issue.id) + end + + it 'returns issues matching given search string for description' do + get api("#{base_url}/issues?search=#{issue.description}", user) + + expect_paginated_array_response(issue.id) + end + + it 'returns an array of issues found by iids' do + get api("#{base_url}/issues", user), params: { iids: [issue.iid] } + + expect_paginated_array_response(issue.id) + end + + it 'returns an empty array if iid does not exist' do + get api("#{base_url}/issues", user), params: { iids: [0] } + + expect_paginated_array_response([]) + end + + it 'returns an empty array if not all labels matches' do + get api("#{base_url}/issues?labels=#{label.title},foo", user) + + expect_paginated_array_response([]) + end + + it 'returns an array of project issues with any label' do + get api("#{base_url}/issues", user), params: { labels: IssuesFinder::FILTER_ANY } + + expect_paginated_array_response(issue.id) + end + + it 'returns an array of project issues with any label with labels param as array' do + get api("#{base_url}/issues", user), params: { labels: [IssuesFinder::FILTER_ANY] } + + expect_paginated_array_response(issue.id) + end + + it 'returns an array of project issues with no label' do + get api("#{base_url}/issues", user), params: { labels: IssuesFinder::FILTER_NONE } + + expect_paginated_array_response([confidential_issue.id, closed_issue.id]) + end + + it 'returns an array of project issues with no label with labels param as array' do + get api("#{base_url}/issues", user), params: { labels: [IssuesFinder::FILTER_NONE] } + + expect_paginated_array_response([confidential_issue.id, closed_issue.id]) + end + + it 'returns an empty array if no project issue matches labels' do + get api("#{base_url}/issues", user), params: { labels: 'foo,bar' } + + expect_paginated_array_response([]) + end + + it 'returns an empty array if no issue matches milestone' do + get api("#{base_url}/issues", user), params: { milestone: empty_milestone.title } + + expect_paginated_array_response([]) + end + + it 'returns an empty array if milestone does not exist' do + get api("#{base_url}/issues", user), params: { milestone: :foo } + + expect_paginated_array_response([]) + end + + it 'returns an array of issues in given milestone' do + get api("#{base_url}/issues", user), params: { milestone: milestone.title } + + expect_paginated_array_response([issue.id, closed_issue.id]) + end + + it 'returns an array of issues matching state in milestone' do + get api("#{base_url}/issues", user), params: { milestone: milestone.title, state: :closed } + + expect_paginated_array_response(closed_issue.id) + end + + it 'returns an array of issues with no milestone' do + get api("#{base_url}/issues", user), params: { milestone: no_milestone_title } + + expect_paginated_array_response(confidential_issue.id) + end + + it 'returns an array of issues with any milestone' do + get api("#{base_url}/issues", user), params: { milestone: any_milestone_title } + + expect_paginated_array_response([issue.id, closed_issue.id]) + end + + context 'without sort params' do + it 'sorts by created_at descending by default' do + get api("#{base_url}/issues", user) + + expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id]) + end + + context 'with 2 issues with same created_at' do + let!(:closed_issue2) do + create :closed_issue, + author: user, + assignees: [user], + project: project, + milestone: milestone, + created_at: closed_issue.created_at, + updated_at: 1.hour.ago, + title: issue_title, + description: issue_description + end + + it 'page breaks first page correctly' do + get api("#{base_url}/issues?per_page=3", user) + + expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue2.id]) + end + + it 'page breaks second page correctly' do + get api("#{base_url}/issues?per_page=3&page=2", user) + + expect_paginated_array_response([closed_issue.id]) + end + end + end + + it 'sorts ascending when requested' do + get api("#{base_url}/issues", user), params: { sort: :asc } + + expect_paginated_array_response([closed_issue.id, confidential_issue.id, issue.id]) + end + + it 'sorts by updated_at descending when requested' do + get api("#{base_url}/issues", user), params: { order_by: :updated_at } + + issue.touch(:updated_at) + + expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id]) + end + + it 'sorts by updated_at ascending when requested' do + get api("#{base_url}/issues", user), params: { order_by: :updated_at, sort: :asc } + + expect_paginated_array_response([closed_issue.id, confidential_issue.id, issue.id]) + end + + context 'issues_statistics' do + context 'no state is treated as all state' do + let(:params) { {} } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'project issues statistics' + end + + context 'statistics when all state is passed' do + let(:params) { { state: :all } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'project issues statistics' + end + + context 'closed state is treated as all state' do + let(:params) { { state: :closed } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'project issues statistics' + end + + context 'opened state is treated as all state' do + let(:params) { { state: :opened } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'project issues statistics' + end + + context 'when filtering by milestone and no state treated as all state' do + let(:params) { { milestone: milestone.title } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'project issues statistics' + end + + context 'when filtering by milestone and all state' do + let(:params) { { milestone: milestone.title, state: :all } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'project issues statistics' + end + + context 'when filtering by milestone and closed state treated as all state' do + let(:params) { { milestone: milestone.title, state: :closed } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'project issues statistics' + end + + context 'when filtering by milestone and opened state treated as all state' do + let(:params) { { milestone: milestone.title, state: :opened } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'project issues statistics' + end + + context 'sort does not affect statistics ' do + let(:params) { { state: :opened, order_by: 'updated_at' } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'project issues statistics' + end + end + + context 'filtering by assignee_username' do + let(:another_assignee) { create(:assignee) } + let!(:issue1) { create(:issue, author: user2, project: project, created_at: 3.days.ago) } + let!(:issue2) { create(:issue, author: user2, project: project, created_at: 2.days.ago) } + let!(:issue3) { create(:issue, author: user2, assignees: [assignee, another_assignee], project: project, created_at: 1.day.ago) } + + it 'returns issues by assignee_username' do + get api("/issues", user), params: { assignee_username: [assignee.username], scope: 'all' } + + expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id]) + expect_paginated_array_response([confidential_issue.id, issue3.id]) + end + + it 'returns issues by assignee_username as string' do + get api("/issues", user), params: { assignee_username: assignee.username, scope: 'all' } + + expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id]) + expect_paginated_array_response([confidential_issue.id, issue3.id]) + end + + it 'returns error when multiple assignees are passed' do + get api("/issues", user), params: { assignee_username: [assignee.username, another_assignee.username], scope: 'all' } + + expect(response).to have_gitlab_http_status(400) + expect(json_response["error"]).to include("allows one value, but found 2") + end + + it 'returns error when assignee_username and assignee_id are passed together' do + get api("/issues", user), params: { assignee_username: [assignee.username], assignee_id: another_assignee.id, scope: 'all' } + + expect(response).to have_gitlab_http_status(400) + expect(json_response["error"]).to include("mutually exclusive") + end + end + end + + describe 'GET /projects/:id/issues/:issue_iid' do + context 'when unauthenticated' do + it 'returns public issues' do + get api("/projects/#{project.id}/issues/#{issue.iid}") + + expect(response).to have_gitlab_http_status(200) + end + end + + it 'exposes known attributes' do + get api("/projects/#{project.id}/issues/#{issue.iid}", user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['id']).to eq(issue.id) + expect(json_response['iid']).to eq(issue.iid) + expect(json_response['project_id']).to eq(issue.project.id) + expect(json_response['title']).to eq(issue.title) + expect(json_response['description']).to eq(issue.description) + expect(json_response['state']).to eq(issue.state) + expect(json_response['closed_at']).to be_falsy + expect(json_response['created_at']).to be_present + expect(json_response['updated_at']).to be_present + expect(json_response['labels']).to eq(issue.label_names) + expect(json_response['milestone']).to be_a Hash + expect(json_response['assignees']).to be_a Array + expect(json_response['assignee']).to be_a Hash + expect(json_response['author']).to be_a Hash + expect(json_response['confidential']).to be_falsy + end + + it 'exposes the closed_at attribute' do + get api("/projects/#{project.id}/issues/#{closed_issue.iid}", user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['closed_at']).to be_present + end + + context 'links exposure' do + it 'exposes related resources full URIs' do + get api("/projects/#{project.id}/issues/#{issue.iid}", user) + + links = json_response['_links'] + + expect(links['self']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}") + expect(links['notes']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}/notes") + expect(links['award_emoji']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}/award_emoji") + expect(links['project']).to end_with("/api/v4/projects/#{project.id}") + end + end + + it 'returns a project issue by internal id' do + get api("/projects/#{project.id}/issues/#{issue.iid}", user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['title']).to eq(issue.title) + expect(json_response['iid']).to eq(issue.iid) + end + + it 'returns 404 if issue id not found' do + get api("/projects/#{project.id}/issues/54321", user) + expect(response).to have_gitlab_http_status(404) + end + + it 'returns 404 if the issue ID is used' do + get api("/projects/#{project.id}/issues/#{issue.id}", user) + + expect(response).to have_gitlab_http_status(404) + end + + context 'confidential issues' do + it 'returns 404 for non project members' do + get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member) + + expect(response).to have_gitlab_http_status(404) + end + + it 'returns 404 for project members with guest role' do + get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest) + + expect(response).to have_gitlab_http_status(404) + end + + it 'returns confidential issue for project members' do + get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['title']).to eq(confidential_issue.title) + expect(json_response['iid']).to eq(confidential_issue.iid) + end + + it 'returns confidential issue for author' do + get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['title']).to eq(confidential_issue.title) + expect(json_response['iid']).to eq(confidential_issue.iid) + end + + it 'returns confidential issue for assignee' do + get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", assignee) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['title']).to eq(confidential_issue.title) + expect(json_response['iid']).to eq(confidential_issue.iid) + end + + it 'returns confidential issue for admin' do + get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['title']).to eq(confidential_issue.title) + expect(json_response['iid']).to eq(confidential_issue.iid) + end + end + end + + describe 'GET :id/issues/:issue_iid/closed_by' do + let(:merge_request) do + create(:merge_request, + :simple, + author: user, + source_project: project, + target_project: project, + description: "closes #{issue.to_reference}") + end + + before do + create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request) + end + + context 'when unauthenticated' do + it 'return public project issues' do + get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by") + + expect_paginated_array_response(merge_request.id) + end + end + + it 'returns merge requests that will close issue on merge' do + get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by", user) + + expect_paginated_array_response(merge_request.id) + end + + context 'when no merge requests will close issue' do + it 'returns empty array' do + get api("/projects/#{project.id}/issues/#{closed_issue.iid}/closed_by", user) + + expect_paginated_array_response([]) + end + end + + it "returns 404 when issue doesn't exists" do + get api("/projects/#{project.id}/issues/0/closed_by", user) + + expect(response).to have_gitlab_http_status(404) + end + end + + describe 'GET :id/issues/:issue_iid/related_merge_requests' do + def get_related_merge_requests(project_id, issue_iid, user = nil) + get api("/projects/#{project_id}/issues/#{issue_iid}/related_merge_requests", user) + end + + def create_referencing_mr(user, project, issue) + attributes = { + author: user, + source_project: project, + target_project: project, + source_branch: 'master', + target_branch: 'test', + description: "See #{issue.to_reference}" + } + create(:merge_request, attributes).tap do |merge_request| + create(:note, :system, project: issue.project, noteable: issue, author: user, note: merge_request.to_reference(full: true)) + end + end + + let!(:related_mr) { create_referencing_mr(user, project, issue) } + + context 'when unauthenticated' do + it 'return list of referenced merge requests from issue' do + get_related_merge_requests(project.id, issue.iid) + + expect_paginated_array_response(related_mr.id) + end + + it 'renders 404 if project is not visible' do + private_project = create(:project, :private) + private_issue = create(:issue, project: private_project) + create_referencing_mr(user, private_project, private_issue) + + get_related_merge_requests(private_project.id, private_issue.iid) + + expect(response).to have_gitlab_http_status(404) + end + end + + it 'returns merge requests that mentioned a issue' do + create(:merge_request, + :simple, + author: user, + source_project: project, + target_project: project, + description: 'Some description') + + get_related_merge_requests(project.id, issue.iid, user) + + expect_paginated_array_response(related_mr.id) + end + + it 'returns merge requests cross-project wide' do + project2 = create(:project, :public, creator_id: user.id, namespace: user.namespace) + merge_request = create_referencing_mr(user, project2, issue) + + get_related_merge_requests(project.id, issue.iid, user) + + expect_paginated_array_response([related_mr.id, merge_request.id]) + end + + it 'does not generate references to projects with no access' do + private_project = create(:project, :private) + create_referencing_mr(private_project.creator, private_project, issue) + + get_related_merge_requests(project.id, issue.iid, user) + + expect_paginated_array_response(related_mr.id) + end + + context 'no merge request mentioned a issue' do + it 'returns empty array' do + get_related_merge_requests(project.id, closed_issue.iid, user) + + expect_paginated_array_response([]) + end + end + + it "returns 404 when issue doesn't exists" do + get_related_merge_requests(project.id, 0, user) + + expect(response).to have_gitlab_http_status(404) + end + end + + describe 'GET /projects/:id/issues/:issue_iid/user_agent_detail' do + let!(:user_agent_detail) { create(:user_agent_detail, subject: issue) } + + context 'when unauthenticated' do + it 'returns unauthorized' do + get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail") + + expect(response).to have_gitlab_http_status(401) + end + end + + it 'exposes known attributes' do + get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", admin) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['user_agent']).to eq(user_agent_detail.user_agent) + expect(json_response['ip_address']).to eq(user_agent_detail.ip_address) + expect(json_response['akismet_submitted']).to eq(user_agent_detail.submitted) + end + + it 'returns unauthorized for non-admin users' do + get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", user) + + expect(response).to have_gitlab_http_status(403) + end + end + + describe 'GET projects/:id/issues/:issue_iid/participants' do + it_behaves_like 'issuable participants endpoint' do + let(:entity) { issue } + end + + it 'returns 404 if the issue is confidential' do + post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/participants", non_member) + + expect(response).to have_gitlab_http_status(404) + end + end +end diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb new file mode 100644 index 00000000000..9b9cc778fb3 --- /dev/null +++ b/spec/requests/api/issues/issues_spec.rb @@ -0,0 +1,796 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::Issues do + set(:user) { create(:user) } + set(:project) do + create(:project, :public, creator_id: user.id, namespace: user.namespace) + end + + let(:user2) { create(:user) } + let(:non_member) { create(:user) } + set(:guest) { create(:user) } + set(:author) { create(:author) } + set(:assignee) { create(:assignee) } + let(:admin) { create(:user, :admin) } + let(:issue_title) { 'foo' } + let(:issue_description) { 'closed' } + let!(:closed_issue) do + create :closed_issue, + author: user, + assignees: [user], + project: project, + state: :closed, + milestone: milestone, + created_at: generate(:past_time), + updated_at: 3.hours.ago, + closed_at: 1.hour.ago + end + let!(:confidential_issue) do + create :issue, + :confidential, + project: project, + author: author, + assignees: [assignee], + created_at: generate(:past_time), + updated_at: 2.hours.ago + end + let!(:issue) do + create :issue, + author: user, + assignees: [user], + project: project, + milestone: milestone, + created_at: generate(:past_time), + updated_at: 1.hour.ago, + title: issue_title, + description: issue_description + end + set(:label) do + create(:label, title: 'label', color: '#FFAABB', project: project) + end + let!(:label_link) { create(:label_link, label: label, target: issue) } + let(:milestone) { create(:milestone, title: '1.0.0', project: project) } + set(:empty_milestone) do + create(:milestone, title: '2.0.0', project: project) + end + let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) } + + let(:no_milestone_title) { 'None' } + let(:any_milestone_title) { 'Any' } + + before(:all) do + project.add_reporter(user) + project.add_guest(guest) + end + + before do + stub_licensed_features(multiple_issue_assignees: false, issue_weights: false) + end + + shared_examples 'issues statistics' do + it 'returns issues statistics' do + get api("/issues_statistics", user), params: params + + expect(response).to have_gitlab_http_status(200) + expect(json_response['statistics']).not_to be_nil + expect(json_response['statistics']['counts']['all']).to eq counts[:all] + expect(json_response['statistics']['counts']['closed']).to eq counts[:closed] + expect(json_response['statistics']['counts']['opened']).to eq counts[:opened] + end + end + + describe 'GET /issues' do + context 'when unauthenticated' do + it 'returns an array of all issues' do + get api('/issues'), params: { scope: 'all' } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + end + + it 'returns authentication error without any scope' do + get api('/issues') + + expect(response).to have_http_status(401) + end + + it 'returns authentication error when scope is assigned-to-me' do + get api('/issues'), params: { scope: 'assigned-to-me' } + + expect(response).to have_http_status(401) + end + + it 'returns authentication error when scope is created-by-me' do + get api('/issues'), params: { scope: 'created-by-me' } + + expect(response).to have_http_status(401) + end + + it 'returns an array of issues matching state in milestone' do + get api('/issues'), params: { milestone: 'foo', scope: 'all' } + + expect(response).to have_http_status(200) + expect_paginated_array_response([]) + end + + it 'returns an array of issues matching state in milestone' do + get api('/issues'), params: { milestone: milestone.title, scope: 'all' } + + expect(response).to have_http_status(200) + expect_paginated_array_response([issue.id, closed_issue.id]) + end + + context 'issues_statistics' do + it 'returns authentication error without any scope' do + get api('/issues_statistics') + + expect(response).to have_http_status(401) + end + + it 'returns authentication error when scope is assigned_to_me' do + get api('/issues_statistics'), params: { scope: 'assigned_to_me' } + + expect(response).to have_http_status(401) + end + + it 'returns authentication error when scope is created_by_me' do + get api('/issues_statistics'), params: { scope: 'created_by_me' } + + expect(response).to have_http_status(401) + end + + context 'no state is treated as all state' do + let(:params) { {} } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'issues statistics' + end + + context 'statistics when all state is passed' do + let(:params) { { state: :all } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'issues statistics' + end + + context 'closed state is treated as all state' do + let(:params) { { state: :closed } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'issues statistics' + end + + context 'opened state is treated as all state' do + let(:params) { { state: :opened } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'issues statistics' + end + + context 'when filtering by milestone and no state treated as all state' do + let(:params) { { milestone: milestone.title } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'issues statistics' + end + + context 'when filtering by milestone and all state' do + let(:params) { { milestone: milestone.title, state: :all } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'issues statistics' + end + + context 'when filtering by milestone and closed state treated as all state' do + let(:params) { { milestone: milestone.title, state: :closed } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'issues statistics' + end + + context 'when filtering by milestone and opened state treated as all state' do + let(:params) { { milestone: milestone.title, state: :opened } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'issues statistics' + end + + context 'sort does not affect statistics ' do + let(:params) { { state: :opened, order_by: 'updated_at' } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'issues statistics' + end + end + end + + context 'when authenticated' do + it 'returns an array of issues' do + get api('/issues', user) + + expect_paginated_array_response([issue.id, closed_issue.id]) + expect(json_response.first['title']).to eq(issue.title) + expect(json_response.last).to have_key('web_url') + end + + it 'returns an array of closed issues' do + get api('/issues', user), params: { state: :closed } + + expect_paginated_array_response(closed_issue.id) + end + + it 'returns an array of opened issues' do + get api('/issues', user), params: { state: :opened } + + expect_paginated_array_response(issue.id) + end + + it 'returns an array of all issues' do + get api('/issues', user), params: { state: :all } + + expect_paginated_array_response([issue.id, closed_issue.id]) + end + + it 'returns issues assigned to me' do + issue2 = create(:issue, assignees: [user2], project: project) + + get api('/issues', user2), params: { scope: 'assigned_to_me' } + + expect_paginated_array_response(issue2.id) + end + + it 'returns issues assigned to me (kebab-case)' do + issue2 = create(:issue, assignees: [user2], project: project) + + get api('/issues', user2), params: { scope: 'assigned-to-me' } + + expect_paginated_array_response(issue2.id) + end + + it 'returns issues authored by the given author id' do + issue2 = create(:issue, author: user2, project: project) + + get api('/issues', user), params: { author_id: user2.id, scope: 'all' } + + expect_paginated_array_response(issue2.id) + end + + it 'returns issues assigned to the given assignee id' do + issue2 = create(:issue, assignees: [user2], project: project) + + get api('/issues', user), params: { assignee_id: user2.id, scope: 'all' } + + expect_paginated_array_response(issue2.id) + end + + it 'returns issues authored by the given author id and assigned to the given assignee id' do + issue2 = create(:issue, author: user2, assignees: [user2], project: project) + + get api('/issues', user), params: { author_id: user2.id, assignee_id: user2.id, scope: 'all' } + + expect_paginated_array_response(issue2.id) + end + + it 'returns issues with no assignee' do + issue2 = create(:issue, author: user2, project: project) + + get api('/issues', user), params: { assignee_id: 0, scope: 'all' } + + expect_paginated_array_response(issue2.id) + end + + it 'returns issues with no assignee' do + issue2 = create(:issue, author: user2, project: project) + + get api('/issues', user), params: { assignee_id: 'None', scope: 'all' } + + expect_paginated_array_response(issue2.id) + end + + it 'returns issues with any assignee' do + # This issue without assignee should not be returned + create(:issue, author: user2, project: project) + + get api('/issues', user), params: { assignee_id: 'Any', scope: 'all' } + + expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id]) + end + + it 'returns only confidential issues' do + get api('/issues', user), params: { confidential: true, scope: 'all' } + + expect_paginated_array_response(confidential_issue.id) + end + + it 'returns only public issues' do + get api('/issues', user), params: { confidential: false } + + expect_paginated_array_response([issue.id, closed_issue.id]) + end + + it 'returns issues reacted by the authenticated user' do + issue2 = create(:issue, project: project, author: user, assignees: [user]) + create(:award_emoji, awardable: issue2, user: user2, name: 'star') + create(:award_emoji, awardable: issue, user: user2, name: 'thumbsup') + + get api('/issues', user2), params: { my_reaction_emoji: 'Any', scope: 'all' } + + expect_paginated_array_response([issue2.id, issue.id]) + end + + it 'returns issues not reacted by the authenticated user' do + issue2 = create(:issue, project: project, author: user, assignees: [user]) + create(:award_emoji, awardable: issue2, user: user2, name: 'star') + + get api('/issues', user2), params: { my_reaction_emoji: 'None', scope: 'all' } + + expect_paginated_array_response([issue.id, closed_issue.id]) + end + + it 'returns issues matching given search string for title' do + get api('/issues', user), params: { search: issue.title } + + expect_paginated_array_response(issue.id) + end + + it 'returns issues matching given search string for title and scoped in title' do + get api('/issues', user), params: { search: issue.title, in: 'title' } + + expect_paginated_array_response(issue.id) + end + + it 'returns an empty array if no issue matches given search string for title and scoped in description' do + get api('/issues', user), params: { search: issue.title, in: 'description' } + + expect_paginated_array_response([]) + end + + it 'returns issues matching given search string for description' do + get api('/issues', user), params: { search: issue.description } + + expect_paginated_array_response(issue.id) + end + + context 'filtering before a specific date' do + let!(:issue2) { create(:issue, project: project, author: user, created_at: Date.new(2000, 1, 1), updated_at: Date.new(2000, 1, 1)) } + + it 'returns issues created before a specific date' do + get api('/issues?created_before=2000-01-02T00:00:00.060Z', user) + + expect_paginated_array_response(issue2.id) + end + + it 'returns issues updated before a specific date' do + get api('/issues?updated_before=2000-01-02T00:00:00.060Z', user) + + expect_paginated_array_response(issue2.id) + end + end + + context 'filtering after a specific date' do + let!(:issue2) { create(:issue, project: project, author: user, created_at: 1.week.from_now, updated_at: 1.week.from_now) } + + it 'returns issues created after a specific date' do + get api("/issues?created_after=#{issue2.created_at}", user) + + expect_paginated_array_response(issue2.id) + end + + it 'returns issues updated after a specific date' do + get api("/issues?updated_after=#{issue2.updated_at}", user) + + expect_paginated_array_response(issue2.id) + end + end + + context 'filter by labels or label_name param' do + context 'N+1' do + let(:label_b) { create(:label, title: 'foo', project: project) } + let(:label_c) { create(:label, title: 'bar', project: project) } + + before do + create(:label_link, label: label_b, target: issue) + create(:label_link, label: label_c, target: issue) + end + it 'tests N+1' do + control = ActiveRecord::QueryRecorder.new do + get api('/issues', user), params: { labels: [label.title, label_b.title, label_c.title] } + end + + label_d = create(:label, title: 'dar', project: project) + label_e = create(:label, title: 'ear', project: project) + create(:label_link, label: label_d, target: issue) + create(:label_link, label: label_e, target: issue) + + expect do + get api('/issues', user), params: { labels: [label.title, label_b.title, label_c.title] } + end.not_to exceed_query_limit(control) + expect(issue.labels.count).to eq(5) + end + end + + it 'returns an array of labeled issues' do + get api('/issues', user), params: { labels: label.title } + + expect_paginated_array_response(issue.id) + expect(json_response.first['labels']).to eq([label.title]) + end + + it 'returns an array of labeled issues with labels param as array' do + get api('/issues', user), params: { labels: [label.title] } + + expect_paginated_array_response(issue.id) + expect(json_response.first['labels']).to eq([label.title]) + end + + context 'with labeled issues' do + let(:label_b) { create(:label, title: 'foo', project: project) } + let(:label_c) { create(:label, title: 'bar', project: project) } + + before do + create(:label_link, label: label_b, target: issue) + create(:label_link, label: label_c, target: issue) + + get api('/issues', user), params: params + end + + it_behaves_like 'labeled issues with labels and label_name params' + end + + it 'returns an empty array if no issue matches labels' do + get api('/issues', user), params: { labels: 'foo,bar' } + + expect_paginated_array_response([]) + end + + it 'returns an empty array if no issue matches labels with labels param as array' do + get api('/issues', user), params: { labels: %w(foo bar) } + + expect_paginated_array_response([]) + end + + it 'returns an array of labeled issues matching given state' do + get api('/issues', user), params: { labels: label.title, state: :opened } + + expect_paginated_array_response(issue.id) + expect(json_response.first['labels']).to eq([label.title]) + expect(json_response.first['state']).to eq('opened') + end + + it 'returns an array of labeled issues matching given state with labels param as array' do + get api('/issues', user), params: { labels: [label.title], state: :opened } + + expect_paginated_array_response(issue.id) + expect(json_response.first['labels']).to eq([label.title]) + expect(json_response.first['state']).to eq('opened') + end + + it 'returns an empty array if no issue matches labels and state filters' do + get api('/issues', user), params: { labels: label.title, state: :closed } + + expect_paginated_array_response([]) + end + + it 'returns an array of issues with any label' do + get api('/issues', user), params: { labels: IssuesFinder::FILTER_ANY } + + expect_paginated_array_response(issue.id) + end + + it 'returns an array of issues with any label with labels param as array' do + get api('/issues', user), params: { labels: [IssuesFinder::FILTER_ANY] } + + expect_paginated_array_response(issue.id) + end + + it 'returns an array of issues with no label' do + get api('/issues', user), params: { labels: IssuesFinder::FILTER_NONE } + + expect_paginated_array_response(closed_issue.id) + end + + it 'returns an array of issues with no label with labels param as array' do + get api('/issues', user), params: { labels: [IssuesFinder::FILTER_NONE] } + + expect_paginated_array_response(closed_issue.id) + end + + it 'returns an array of issues with no label when using the legacy No+Label filter' do + get api('/issues', user), params: { labels: 'No Label' } + + expect_paginated_array_response(closed_issue.id) + end + + it 'returns an array of issues with no label when using the legacy No+Label filter with labels param as array' do + get api('/issues', user), params: { labels: ['No Label'] } + + expect_paginated_array_response(closed_issue.id) + end + end + + it 'returns an empty array if no issue matches milestone' do + get api("/issues?milestone=#{empty_milestone.title}", user) + + expect_paginated_array_response([]) + end + + it 'returns an empty array if milestone does not exist' do + get api('/issues?milestone=foo', user) + + expect_paginated_array_response([]) + end + + it 'returns an array of issues in given milestone' do + get api("/issues?milestone=#{milestone.title}", user) + + expect_paginated_array_response([issue.id, closed_issue.id]) + end + + it 'returns an array of issues in given milestone_title param' do + get api("/issues?milestone_title=#{milestone.title}", user) + + expect_paginated_array_response([issue.id, closed_issue.id]) + end + + it 'returns an array of issues matching state in milestone' do + get api("/issues?milestone=#{milestone.title}&state=closed", user) + + expect_paginated_array_response(closed_issue.id) + end + + it 'returns an array of issues with no milestone' do + get api("/issues?milestone=#{no_milestone_title}", author) + + expect_paginated_array_response(confidential_issue.id) + end + + it 'returns an array of issues with no milestone using milestone_title param' do + get api("/issues?milestone_title=#{no_milestone_title}", author) + + expect_paginated_array_response(confidential_issue.id) + end + + it 'returns an array of issues found by iids' do + get api('/issues', user), params: { iids: [closed_issue.iid] } + + expect_paginated_array_response(closed_issue.id) + end + + it 'returns an empty array if iid does not exist' do + get api('/issues', user), params: { iids: [0] } + + expect_paginated_array_response([]) + end + + context 'without sort params' do + it 'sorts by created_at descending by default' do + get api('/issues', user) + + expect_paginated_array_response([issue.id, closed_issue.id]) + end + + context 'with 2 issues with same created_at' do + let!(:closed_issue2) do + create :closed_issue, + author: user, + assignees: [user], + project: project, + milestone: milestone, + created_at: closed_issue.created_at, + updated_at: 1.hour.ago, + title: issue_title, + description: issue_description + end + + it 'page breaks first page correctly' do + get api('/issues?per_page=2', user) + + expect_paginated_array_response([issue.id, closed_issue2.id]) + end + + it 'page breaks second page correctly' do + get api('/issues?per_page=2&page=2', user) + + expect_paginated_array_response([closed_issue.id]) + end + end + end + + it 'sorts ascending when requested' do + get api('/issues?sort=asc', user) + + expect_paginated_array_response([closed_issue.id, issue.id]) + end + + it 'sorts by updated_at descending when requested' do + get api('/issues?order_by=updated_at', user) + + issue.touch(:updated_at) + + expect_paginated_array_response([issue.id, closed_issue.id]) + end + + it 'sorts by updated_at ascending when requested' do + get api('/issues?order_by=updated_at&sort=asc', user) + + issue.touch(:updated_at) + + expect_paginated_array_response([closed_issue.id, issue.id]) + end + + it 'matches V4 response schema' do + get api('/issues', user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/issues') + end + + it 'returns a related merge request count of 0 if there are no related merge requests' do + get api('/issues', user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/issues') + expect(json_response.first).to include('merge_requests_count' => 0) + end + + it 'returns a related merge request count > 0 if there are related merge requests' do + create(:merge_requests_closing_issues, issue: issue) + + get api('/issues', user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/issues') + expect(json_response.first).to include('merge_requests_count' => 1) + end + + context 'issues_statistics' do + context 'no state is treated as all state' do + let(:params) { {} } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'issues statistics' + end + + context 'statistics when all state is passed' do + let(:params) { { state: :all } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'issues statistics' + end + + context 'closed state is treated as all state' do + let(:params) { { state: :closed } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'issues statistics' + end + + context 'opened state is treated as all state' do + let(:params) { { state: :opened } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'issues statistics' + end + + context 'when filtering by milestone and no state treated as all state' do + let(:params) { { milestone: milestone.title } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'issues statistics' + end + + context 'when filtering by milestone and all state' do + let(:params) { { milestone: milestone.title, state: :all } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'issues statistics' + end + + context 'when filtering by milestone and closed state treated as all state' do + let(:params) { { milestone: milestone.title, state: :closed } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'issues statistics' + end + + context 'when filtering by milestone and opened state treated as all state' do + let(:params) { { milestone: milestone.title, state: :opened } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'issues statistics' + end + + context 'sort does not affect statistics ' do + let(:params) { { state: :opened, order_by: 'updated_at' } } + let(:counts) { { all: 2, closed: 1, opened: 1 } } + + it_behaves_like 'issues statistics' + end + end + + context 'filtering by assignee_username' do + let(:another_assignee) { create(:assignee) } + let!(:issue1) { create(:issue, author: user2, project: project, created_at: 3.days.ago) } + let!(:issue2) { create(:issue, author: user2, project: project, created_at: 2.days.ago) } + let!(:issue3) { create(:issue, author: user2, assignees: [assignee, another_assignee], project: project, created_at: 1.day.ago) } + + it 'returns issues with by assignee_username' do + get api("/issues", user), params: { assignee_username: [assignee.username], scope: 'all' } + + expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id]) + expect_paginated_array_response([confidential_issue.id, issue3.id]) + end + + it 'returns issues by assignee_username as string' do + get api("/issues", user), params: { assignee_username: assignee.username, scope: 'all' } + + expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id]) + expect_paginated_array_response([confidential_issue.id, issue3.id]) + end + + it 'returns error when multiple assignees are passed' do + get api("/issues", user), params: { assignee_username: [assignee.username, another_assignee.username], scope: 'all' } + + expect(response).to have_gitlab_http_status(400) + expect(json_response["error"]).to include("allows one value, but found 2") + end + + it 'returns error when assignee_username and assignee_id are passed together' do + get api("/issues", user), params: { assignee_username: [assignee.username], assignee_id: another_assignee.id, scope: 'all' } + + expect(response).to have_gitlab_http_status(400) + expect(json_response["error"]).to include("mutually exclusive") + end + end + end + end + + describe 'DELETE /projects/:id/issues/:issue_iid' do + it 'rejects a non member from deleting an issue' do + delete api("/projects/#{project.id}/issues/#{issue.iid}", non_member) + expect(response).to have_gitlab_http_status(403) + end + + it 'rejects a developer from deleting an issue' do + delete api("/projects/#{project.id}/issues/#{issue.iid}", author) + expect(response).to have_gitlab_http_status(403) + end + + context 'when the user is project owner' do + let(:owner) { create(:user) } + let(:project) { create(:project, namespace: owner.namespace) } + + it 'deletes the issue if an admin requests it' do + delete api("/projects/#{project.id}/issues/#{issue.iid}", owner) + + expect(response).to have_gitlab_http_status(204) + end + + it_behaves_like '412 response' do + let(:request) { api("/projects/#{project.id}/issues/#{issue.iid}", owner) } + end + end + + context 'when issue does not exist' do + it 'returns 404 when trying to move an issue' do + delete api("/projects/#{project.id}/issues/123", user) + + expect(response).to have_gitlab_http_status(404) + end + end + + it 'returns 404 when using the issue ID instead of IID' do + delete api("/projects/#{project.id}/issues/#{issue.id}", user) + + expect(response).to have_gitlab_http_status(404) + end + end + + describe 'time tracking endpoints' do + let(:issuable) { issue } + + include_examples 'time tracking endpoints', 'issue' + end +end diff --git a/spec/requests/api/issues/post_projects_issues_spec.rb b/spec/requests/api/issues/post_projects_issues_spec.rb new file mode 100644 index 00000000000..b74e8867310 --- /dev/null +++ b/spec/requests/api/issues/post_projects_issues_spec.rb @@ -0,0 +1,549 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::Issues do + set(:user) { create(:user) } + set(:project) do + create(:project, :public, creator_id: user.id, namespace: user.namespace) + end + + let(:user2) { create(:user) } + let(:non_member) { create(:user) } + set(:guest) { create(:user) } + set(:author) { create(:author) } + set(:assignee) { create(:assignee) } + let(:admin) { create(:user, :admin) } + let(:issue_title) { 'foo' } + let(:issue_description) { 'closed' } + let!(:closed_issue) do + create :closed_issue, + author: user, + assignees: [user], + project: project, + state: :closed, + milestone: milestone, + created_at: generate(:past_time), + updated_at: 3.hours.ago, + closed_at: 1.hour.ago + end + let!(:confidential_issue) do + create :issue, + :confidential, + project: project, + author: author, + assignees: [assignee], + created_at: generate(:past_time), + updated_at: 2.hours.ago + end + let!(:issue) do + create :issue, + author: user, + assignees: [user], + project: project, + milestone: milestone, + created_at: generate(:past_time), + updated_at: 1.hour.ago, + title: issue_title, + description: issue_description + end + set(:label) do + create(:label, title: 'label', color: '#FFAABB', project: project) + end + let!(:label_link) { create(:label_link, label: label, target: issue) } + let(:milestone) { create(:milestone, title: '1.0.0', project: project) } + set(:empty_milestone) do + create(:milestone, title: '2.0.0', project: project) + end + let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) } + + let(:no_milestone_title) { 'None' } + let(:any_milestone_title) { 'Any' } + + before(:all) do + project.add_reporter(user) + project.add_guest(guest) + end + + before do + stub_licensed_features(multiple_issue_assignees: false, issue_weights: false) + end + + describe 'POST /projects/:id/issues' do + context 'support for deprecated assignee_id' do + it 'creates a new project issue' do + post api("/projects/#{project.id}/issues", user), + params: { title: 'new issue', assignee_id: user2.id } + + expect(response).to have_gitlab_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['assignee']['name']).to eq(user2.name) + expect(json_response['assignees'].first['name']).to eq(user2.name) + end + + it 'creates a new project issue when assignee_id is empty' do + post api("/projects/#{project.id}/issues", user), + params: { title: 'new issue', assignee_id: '' } + + expect(response).to have_gitlab_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['assignee']).to be_nil + end + end + + context 'single assignee restrictions' do + it 'creates a new project issue with no more than one assignee' do + post api("/projects/#{project.id}/issues", user), + params: { title: 'new issue', assignee_ids: [user2.id, guest.id] } + + expect(response).to have_gitlab_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['assignees'].count).to eq(1) + end + end + + context 'user does not have permissions to create issue' do + let(:not_member) { create(:user) } + + before do + project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE) + end + + it 'renders 403' do + post api("/projects/#{project.id}/issues", not_member), params: { title: 'new issue' } + + expect(response).to have_gitlab_http_status(403) + end + end + + context 'an internal ID is provided' do + context 'by an admin' do + it 'sets the internal ID on the new issue' do + post api("/projects/#{project.id}/issues", admin), + params: { title: 'new issue', iid: 9001 } + + expect(response).to have_gitlab_http_status(201) + expect(json_response['iid']).to eq 9001 + end + end + + context 'by an owner' do + it 'sets the internal ID on the new issue' do + post api("/projects/#{project.id}/issues", user), + params: { title: 'new issue', iid: 9001 } + + expect(response).to have_gitlab_http_status(201) + expect(json_response['iid']).to eq 9001 + end + end + + context 'by a group owner' do + let(:group) { create(:group) } + let(:group_project) { create(:project, :public, namespace: group) } + + it 'sets the internal ID on the new issue' do + group.add_owner(user2) + post api("/projects/#{group_project.id}/issues", user2), + params: { title: 'new issue', iid: 9001 } + + expect(response).to have_gitlab_http_status(201) + expect(json_response['iid']).to eq 9001 + end + end + + context 'by another user' do + it 'ignores the given internal ID' do + post api("/projects/#{project.id}/issues", user2), + params: { title: 'new issue', iid: 9001 } + + expect(response).to have_gitlab_http_status(201) + expect(json_response['iid']).not_to eq 9001 + end + end + end + + it 'creates a new project issue' do + post api("/projects/#{project.id}/issues", user), + params: { title: 'new issue', labels: 'label, label2', weight: 3, assignee_ids: [user2.id] } + + expect(response).to have_gitlab_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['description']).to be_nil + expect(json_response['labels']).to eq(%w(label label2)) + expect(json_response['confidential']).to be_falsy + expect(json_response['assignee']['name']).to eq(user2.name) + expect(json_response['assignees'].first['name']).to eq(user2.name) + end + + it 'creates a new project issue with labels param as array' do + post api("/projects/#{project.id}/issues", user), + params: { title: 'new issue', labels: %w(label label2), weight: 3, assignee_ids: [user2.id] } + + expect(response).to have_gitlab_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['description']).to be_nil + expect(json_response['labels']).to eq(%w(label label2)) + expect(json_response['confidential']).to be_falsy + expect(json_response['assignee']['name']).to eq(user2.name) + expect(json_response['assignees'].first['name']).to eq(user2.name) + end + + it 'creates a new confidential project issue' do + post api("/projects/#{project.id}/issues", user), + params: { title: 'new issue', confidential: true } + + expect(response).to have_gitlab_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['confidential']).to be_truthy + end + + it 'creates a new confidential project issue with a different param' do + post api("/projects/#{project.id}/issues", user), + params: { title: 'new issue', confidential: 'y' } + + expect(response).to have_gitlab_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['confidential']).to be_truthy + end + + it 'creates a public issue when confidential param is false' do + post api("/projects/#{project.id}/issues", user), + params: { title: 'new issue', confidential: false } + + expect(response).to have_gitlab_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['confidential']).to be_falsy + end + + it 'creates a public issue when confidential param is invalid' do + post api("/projects/#{project.id}/issues", user), + params: { title: 'new issue', confidential: 'foo' } + + expect(response).to have_gitlab_http_status(400) + expect(json_response['error']).to eq('confidential is invalid') + end + + it 'returns a 400 bad request if title not given' do + post api("/projects/#{project.id}/issues", user), params: { labels: 'label, label2' } + expect(response).to have_gitlab_http_status(400) + end + + it 'allows special label names' do + post api("/projects/#{project.id}/issues", user), + params: { + title: 'new issue', + labels: 'label, label?, label&foo, ?, &' + } + expect(response.status).to eq(201) + expect(json_response['labels']).to include 'label' + expect(json_response['labels']).to include 'label?' + expect(json_response['labels']).to include 'label&foo' + expect(json_response['labels']).to include '?' + expect(json_response['labels']).to include '&' + end + + it 'allows special label names with labels param as array' do + post api("/projects/#{project.id}/issues", user), + params: { + title: 'new issue', + labels: ['label', 'label?', 'label&foo, ?, &'] + } + expect(response.status).to eq(201) + expect(json_response['labels']).to include 'label' + expect(json_response['labels']).to include 'label?' + expect(json_response['labels']).to include 'label&foo' + expect(json_response['labels']).to include '?' + expect(json_response['labels']).to include '&' + end + + it 'returns 400 if title is too long' do + post api("/projects/#{project.id}/issues", user), + params: { title: 'g' * 256 } + expect(response).to have_gitlab_http_status(400) + expect(json_response['message']['title']).to eq([ + 'is too long (maximum is 255 characters)' + ]) + end + + context 'resolving discussions' do + let(:discussion) { create(:diff_note_on_merge_request).to_discussion } + let(:merge_request) { discussion.noteable } + let(:project) { merge_request.source_project } + + before do + project.add_maintainer(user) + end + + context 'resolving all discussions in a merge request' do + before do + post api("/projects/#{project.id}/issues", user), + params: { + title: 'New Issue', + merge_request_to_resolve_discussions_of: merge_request.iid + } + end + + it_behaves_like 'creating an issue resolving discussions through the API' + end + + context 'resolving a single discussion' do + before do + post api("/projects/#{project.id}/issues", user), + params: { + title: 'New Issue', + merge_request_to_resolve_discussions_of: merge_request.iid, + discussion_to_resolve: discussion.id + } + end + + it_behaves_like 'creating an issue resolving discussions through the API' + end + end + + context 'with due date' do + it 'creates a new project issue' do + due_date = 2.weeks.from_now.strftime('%Y-%m-%d') + + post api("/projects/#{project.id}/issues", user), + params: { title: 'new issue', due_date: due_date } + + expect(response).to have_gitlab_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['description']).to be_nil + expect(json_response['due_date']).to eq(due_date) + end + end + + context 'setting created_at' do + let(:creation_time) { 2.weeks.ago } + let(:params) { { title: 'new issue', labels: 'label, label2', created_at: creation_time } } + + context 'by an admin' do + it 'sets the creation time on the new issue' do + post api("/projects/#{project.id}/issues", admin), params: params + + expect(response).to have_gitlab_http_status(201) + expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time) + end + end + + context 'by a project owner' do + it 'sets the creation time on the new issue' do + post api("/projects/#{project.id}/issues", user), params: params + + expect(response).to have_gitlab_http_status(201) + expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time) + end + end + + context 'by a group owner' do + it 'sets the creation time on the new issue' do + group = create(:group) + group_project = create(:project, :public, namespace: group) + group.add_owner(user2) + post api("/projects/#{group_project.id}/issues", user2), params: params + + expect(response).to have_gitlab_http_status(201) + expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time) + end + end + + context 'by another user' do + it 'ignores the given creation time' do + post api("/projects/#{project.id}/issues", user2), params: params + + expect(response).to have_gitlab_http_status(201) + expect(Time.parse(json_response['created_at'])).not_to be_like_time(creation_time) + end + end + end + + context 'the user can only read the issue' do + it 'cannot create new labels' do + expect do + post api("/projects/#{project.id}/issues", non_member), params: { title: 'new issue', labels: 'label, label2' } + end.not_to change { project.labels.count } + end + + it 'cannot create new labels with labels param as array' do + expect do + post api("/projects/#{project.id}/issues", non_member), params: { title: 'new issue', labels: %w(label label2) } + end.not_to change { project.labels.count } + end + end + end + + describe 'POST /projects/:id/issues with spam filtering' do + before do + allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true) + allow_any_instance_of(AkismetService).to receive_messages(spam?: true) + end + + let(:params) do + { + title: 'new issue', + description: 'content here', + labels: 'label, label2' + } + end + + it 'does not create a new project issue' do + expect { post api("/projects/#{project.id}/issues", user), params: params }.not_to change(Issue, :count) + expect(response).to have_gitlab_http_status(400) + expect(json_response['message']).to eq({ 'error' => 'Spam detected' }) + + spam_logs = SpamLog.all + expect(spam_logs.count).to eq(1) + expect(spam_logs[0].title).to eq('new issue') + expect(spam_logs[0].description).to eq('content here') + expect(spam_logs[0].user).to eq(user) + expect(spam_logs[0].noteable_type).to eq('Issue') + end + end + + describe '/projects/:id/issues/:issue_iid/move' do + let!(:target_project) { create(:project, creator_id: user.id, namespace: user.namespace ) } + let!(:target_project2) { create(:project, creator_id: non_member.id, namespace: non_member.namespace ) } + + it 'moves an issue' do + post api("/projects/#{project.id}/issues/#{issue.iid}/move", user), + params: { to_project_id: target_project.id } + + expect(response).to have_gitlab_http_status(201) + expect(json_response['project_id']).to eq(target_project.id) + end + + context 'when source and target projects are the same' do + it 'returns 400 when trying to move an issue' do + post api("/projects/#{project.id}/issues/#{issue.iid}/move", user), + params: { to_project_id: project.id } + + expect(response).to have_gitlab_http_status(400) + expect(json_response['message']).to eq('Cannot move issue to project it originates from!') + end + end + + context 'when the user does not have the permission to move issues' do + it 'returns 400 when trying to move an issue' do + post api("/projects/#{project.id}/issues/#{issue.iid}/move", user), + params: { to_project_id: target_project2.id } + + expect(response).to have_gitlab_http_status(400) + expect(json_response['message']).to eq('Cannot move issue due to insufficient permissions!') + end + end + + it 'moves the issue to another namespace if I am admin' do + post api("/projects/#{project.id}/issues/#{issue.iid}/move", admin), + params: { to_project_id: target_project2.id } + + expect(response).to have_gitlab_http_status(201) + expect(json_response['project_id']).to eq(target_project2.id) + end + + context 'when using the issue ID instead of iid' do + it 'returns 404 when trying to move an issue' do + post api("/projects/#{project.id}/issues/#{issue.id}/move", user), + params: { to_project_id: target_project.id } + + expect(response).to have_gitlab_http_status(404) + expect(json_response['message']).to eq('404 Issue Not Found') + end + end + + context 'when issue does not exist' do + it 'returns 404 when trying to move an issue' do + post api("/projects/#{project.id}/issues/123/move", user), + params: { to_project_id: target_project.id } + + expect(response).to have_gitlab_http_status(404) + expect(json_response['message']).to eq('404 Issue Not Found') + end + end + + context 'when source project does not exist' do + it 'returns 404 when trying to move an issue' do + post api("/projects/0/issues/#{issue.iid}/move", user), + params: { to_project_id: target_project.id } + + expect(response).to have_gitlab_http_status(404) + expect(json_response['message']).to eq('404 Project Not Found') + end + end + + context 'when target project does not exist' do + it 'returns 404 when trying to move an issue' do + post api("/projects/#{project.id}/issues/#{issue.iid}/move", user), + params: { to_project_id: 0 } + + expect(response).to have_gitlab_http_status(404) + end + end + end + + describe 'POST :id/issues/:issue_iid/subscribe' do + it 'subscribes to an issue' do + post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user2) + + expect(response).to have_gitlab_http_status(201) + expect(json_response['subscribed']).to eq(true) + end + + it 'returns 304 if already subscribed' do + post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user) + + expect(response).to have_gitlab_http_status(304) + end + + it 'returns 404 if the issue is not found' do + post api("/projects/#{project.id}/issues/123/subscribe", user) + + expect(response).to have_gitlab_http_status(404) + end + + it 'returns 404 if the issue ID is used instead of the iid' do + post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user) + + expect(response).to have_gitlab_http_status(404) + end + + it 'returns 404 if the issue is confidential' do + post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/subscribe", non_member) + + expect(response).to have_gitlab_http_status(404) + end + end + + describe 'POST :id/issues/:issue_id/unsubscribe' do + it 'unsubscribes from an issue' do + post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user) + + expect(response).to have_gitlab_http_status(201) + expect(json_response['subscribed']).to eq(false) + end + + it 'returns 304 if not subscribed' do + post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user2) + + expect(response).to have_gitlab_http_status(304) + end + + it 'returns 404 if the issue is not found' do + post api("/projects/#{project.id}/issues/123/unsubscribe", user) + + expect(response).to have_gitlab_http_status(404) + end + + it 'returns 404 if using the issue ID instead of iid' do + post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user) + + expect(response).to have_gitlab_http_status(404) + end + + it 'returns 404 if the issue is confidential' do + post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/unsubscribe", non_member) + + expect(response).to have_gitlab_http_status(404) + end + end +end diff --git a/spec/requests/api/issues/put_projects_issues_spec.rb b/spec/requests/api/issues/put_projects_issues_spec.rb new file mode 100644 index 00000000000..267cba93713 --- /dev/null +++ b/spec/requests/api/issues/put_projects_issues_spec.rb @@ -0,0 +1,392 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::Issues do + set(:user) { create(:user) } + set(:project) do + create(:project, :public, creator_id: user.id, namespace: user.namespace) + end + + let(:user2) { create(:user) } + let(:non_member) { create(:user) } + set(:guest) { create(:user) } + set(:author) { create(:author) } + set(:assignee) { create(:assignee) } + let(:admin) { create(:user, :admin) } + let(:issue_title) { 'foo' } + let(:issue_description) { 'closed' } + let!(:closed_issue) do + create :closed_issue, + author: user, + assignees: [user], + project: project, + state: :closed, + milestone: milestone, + created_at: generate(:past_time), + updated_at: 3.hours.ago, + closed_at: 1.hour.ago + end + let!(:confidential_issue) do + create :issue, + :confidential, + project: project, + author: author, + assignees: [assignee], + created_at: generate(:past_time), + updated_at: 2.hours.ago + end + let!(:issue) do + create :issue, + author: user, + assignees: [user], + project: project, + milestone: milestone, + created_at: generate(:past_time), + updated_at: 1.hour.ago, + title: issue_title, + description: issue_description + end + set(:label) do + create(:label, title: 'label', color: '#FFAABB', project: project) + end + let!(:label_link) { create(:label_link, label: label, target: issue) } + let(:milestone) { create(:milestone, title: '1.0.0', project: project) } + set(:empty_milestone) do + create(:milestone, title: '2.0.0', project: project) + end + let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) } + + let(:no_milestone_title) { 'None' } + let(:any_milestone_title) { 'Any' } + + before(:all) do + project.add_reporter(user) + project.add_guest(guest) + end + + before do + stub_licensed_features(multiple_issue_assignees: false, issue_weights: false) + end + + describe 'PUT /projects/:id/issues/:issue_iid to update only title' do + it 'updates a project issue' do + put api("/projects/#{project.id}/issues/#{issue.iid}", user), + params: { title: 'updated title' } + expect(response).to have_gitlab_http_status(200) + + expect(json_response['title']).to eq('updated title') + end + + it 'returns 404 error if issue iid not found' do + put api("/projects/#{project.id}/issues/44444", user), + params: { title: 'updated title' } + expect(response).to have_gitlab_http_status(404) + end + + it 'returns 404 error if issue id is used instead of the iid' do + put api("/projects/#{project.id}/issues/#{issue.id}", user), + params: { title: 'updated title' } + expect(response).to have_gitlab_http_status(404) + end + + it 'allows special label names' do + put api("/projects/#{project.id}/issues/#{issue.iid}", user), + params: { + title: 'updated title', + labels: 'label, label?, label&foo, ?, &' + } + + expect(response.status).to eq(200) + expect(json_response['labels']).to include 'label' + expect(json_response['labels']).to include 'label?' + expect(json_response['labels']).to include 'label&foo' + expect(json_response['labels']).to include '?' + expect(json_response['labels']).to include '&' + end + + it 'allows special label names with labels param as array' do + put api("/projects/#{project.id}/issues/#{issue.iid}", user), + params: { + title: 'updated title', + labels: ['label', 'label?', 'label&foo, ?, &'] + } + + expect(response.status).to eq(200) + expect(json_response['labels']).to include 'label' + expect(json_response['labels']).to include 'label?' + expect(json_response['labels']).to include 'label&foo' + expect(json_response['labels']).to include '?' + expect(json_response['labels']).to include '&' + end + + context 'confidential issues' do + it 'returns 403 for non project members' do + put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member), + params: { title: 'updated title' } + expect(response).to have_gitlab_http_status(403) + end + + it 'returns 403 for project members with guest role' do + put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest), + params: { title: 'updated title' } + expect(response).to have_gitlab_http_status(403) + end + + it 'updates a confidential issue for project members' do + put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user), + params: { title: 'updated title' } + expect(response).to have_gitlab_http_status(200) + expect(json_response['title']).to eq('updated title') + end + + it 'updates a confidential issue for author' do + put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author), + params: { title: 'updated title' } + expect(response).to have_gitlab_http_status(200) + expect(json_response['title']).to eq('updated title') + end + + it 'updates a confidential issue for admin' do + put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin), + params: { title: 'updated title' } + expect(response).to have_gitlab_http_status(200) + expect(json_response['title']).to eq('updated title') + end + + it 'sets an issue to confidential' do + put api("/projects/#{project.id}/issues/#{issue.iid}", user), + params: { confidential: true } + + expect(response).to have_gitlab_http_status(200) + expect(json_response['confidential']).to be_truthy + end + + it 'makes a confidential issue public' do + put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user), + params: { confidential: false } + + expect(response).to have_gitlab_http_status(200) + expect(json_response['confidential']).to be_falsy + end + + it 'does not update a confidential issue with wrong confidential flag' do + put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user), + params: { confidential: 'foo' } + + expect(response).to have_gitlab_http_status(400) + expect(json_response['error']).to eq('confidential is invalid') + end + end + end + + describe 'PUT /projects/:id/issues/:issue_iid with spam filtering' do + let(:params) do + { + title: 'updated title', + description: 'content here', + labels: 'label, label2' + } + end + + it 'does not create a new project issue' do + allow_any_instance_of(SpamService).to receive_messages(check_for_spam?: true) + allow_any_instance_of(AkismetService).to receive_messages(spam?: true) + + put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: params + + expect(response).to have_gitlab_http_status(400) + expect(json_response['message']).to eq({ 'error' => 'Spam detected' }) + + spam_logs = SpamLog.all + expect(spam_logs.count).to eq(1) + expect(spam_logs[0].title).to eq('updated title') + expect(spam_logs[0].description).to eq('content here') + expect(spam_logs[0].user).to eq(user) + expect(spam_logs[0].noteable_type).to eq('Issue') + end + end + + describe 'PUT /projects/:id/issues/:issue_iid to update assignee' do + context 'support for deprecated assignee_id' do + it 'removes assignee' do + put api("/projects/#{project.id}/issues/#{issue.iid}", user), + params: { assignee_id: 0 } + + expect(response).to have_gitlab_http_status(200) + + expect(json_response['assignee']).to be_nil + end + + it 'updates an issue with new assignee' do + put api("/projects/#{project.id}/issues/#{issue.iid}", user), + params: { assignee_id: user2.id } + + expect(response).to have_gitlab_http_status(200) + + expect(json_response['assignee']['name']).to eq(user2.name) + end + end + + it 'removes assignee' do + put api("/projects/#{project.id}/issues/#{issue.iid}", user), + params: { assignee_ids: [0] } + + expect(response).to have_gitlab_http_status(200) + + expect(json_response['assignees']).to be_empty + end + + it 'updates an issue with new assignee' do + put api("/projects/#{project.id}/issues/#{issue.iid}", user), + params: { assignee_ids: [user2.id] } + + expect(response).to have_gitlab_http_status(200) + + expect(json_response['assignees'].first['name']).to eq(user2.name) + end + + context 'single assignee restrictions' do + it 'updates an issue with several assignees but only one has been applied' do + put api("/projects/#{project.id}/issues/#{issue.iid}", user), + params: { assignee_ids: [user2.id, guest.id] } + + expect(response).to have_gitlab_http_status(200) + + expect(json_response['assignees'].size).to eq(1) + end + end + end + + describe 'PUT /projects/:id/issues/:issue_iid to update labels' do + let!(:label) { create(:label, title: 'dummy', project: project) } + let!(:label_link) { create(:label_link, label: label, target: issue) } + + it 'does not update labels if not present' do + put api("/projects/#{project.id}/issues/#{issue.iid}", user), + params: { title: 'updated title' } + expect(response).to have_gitlab_http_status(200) + expect(json_response['labels']).to eq([label.title]) + end + + it 'removes all labels and touches the record' do + Timecop.travel(1.minute.from_now) do + put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { labels: '' } + end + + expect(response).to have_gitlab_http_status(200) + expect(json_response['labels']).to eq([]) + expect(json_response['updated_at']).to be > Time.now + end + + it 'removes all labels and touches the record with labels param as array' do + Timecop.travel(1.minute.from_now) do + put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { labels: [''] } + end + + expect(response).to have_gitlab_http_status(200) + expect(json_response['labels']).to eq([]) + expect(json_response['updated_at']).to be > Time.now + end + + it 'updates labels and touches the record' do + Timecop.travel(1.minute.from_now) do + put api("/projects/#{project.id}/issues/#{issue.iid}", user), + params: { labels: 'foo,bar' } + end + expect(response).to have_gitlab_http_status(200) + expect(json_response['labels']).to include 'foo' + expect(json_response['labels']).to include 'bar' + expect(json_response['updated_at']).to be > Time.now + end + + it 'updates labels and touches the record with labels param as array' do + Timecop.travel(1.minute.from_now) do + put api("/projects/#{project.id}/issues/#{issue.iid}", user), + params: { labels: %w(foo bar) } + end + expect(response).to have_gitlab_http_status(200) + expect(json_response['labels']).to include 'foo' + expect(json_response['labels']).to include 'bar' + expect(json_response['updated_at']).to be > Time.now + end + + it 'allows special label names' do + put api("/projects/#{project.id}/issues/#{issue.iid}", user), + params: { labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&' } + expect(response.status).to eq(200) + expect(json_response['labels']).to include 'label:foo' + expect(json_response['labels']).to include 'label-bar' + expect(json_response['labels']).to include 'label_bar' + expect(json_response['labels']).to include 'label/bar' + expect(json_response['labels']).to include 'label?bar' + expect(json_response['labels']).to include 'label&bar' + expect(json_response['labels']).to include '?' + expect(json_response['labels']).to include '&' + end + + it 'allows special label names with labels param as array' do + put api("/projects/#{project.id}/issues/#{issue.iid}", user), + params: { labels: ['label:foo', 'label-bar', 'label_bar', 'label/bar,label?bar,label&bar,?,&'] } + expect(response.status).to eq(200) + expect(json_response['labels']).to include 'label:foo' + expect(json_response['labels']).to include 'label-bar' + expect(json_response['labels']).to include 'label_bar' + expect(json_response['labels']).to include 'label/bar' + expect(json_response['labels']).to include 'label?bar' + expect(json_response['labels']).to include 'label&bar' + expect(json_response['labels']).to include '?' + expect(json_response['labels']).to include '&' + end + + it 'returns 400 if title is too long' do + put api("/projects/#{project.id}/issues/#{issue.iid}", user), + params: { title: 'g' * 256 } + expect(response).to have_gitlab_http_status(400) + expect(json_response['message']['title']).to eq([ + 'is too long (maximum is 255 characters)' + ]) + end + end + + describe 'PUT /projects/:id/issues/:issue_iid to update state and label' do + it 'updates a project issue' do + put api("/projects/#{project.id}/issues/#{issue.iid}", user), + params: { labels: 'label2', state_event: 'close' } + expect(response).to have_gitlab_http_status(200) + + expect(json_response['labels']).to include 'label2' + expect(json_response['state']).to eq 'closed' + end + + it 'reopens a project isssue' do + put api("/projects/#{project.id}/issues/#{closed_issue.iid}", user), params: { state_event: 'reopen' } + + expect(response).to have_gitlab_http_status(200) + expect(json_response['state']).to eq 'opened' + end + + context 'when an admin or owner makes the request' do + it 'accepts the update date to be set' do + update_time = 2.weeks.ago + put api("/projects/#{project.id}/issues/#{issue.iid}", user), + params: { labels: 'label3', state_event: 'close', updated_at: update_time } + + expect(response).to have_gitlab_http_status(200) + expect(json_response['labels']).to include 'label3' + expect(Time.parse(json_response['updated_at'])).to be_like_time(update_time) + end + end + end + + describe 'PUT /projects/:id/issues/:issue_iid to update due date' do + it 'creates a new project issue' do + due_date = 2.weeks.from_now.strftime('%Y-%m-%d') + + put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { due_date: due_date } + + expect(response).to have_gitlab_http_status(200) + expect(json_response['due_date']).to eq(due_date) + end + end +end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb deleted file mode 100644 index 0fa34688371..00000000000 --- a/spec/requests/api/issues_spec.rb +++ /dev/null @@ -1,2265 +0,0 @@ -require 'spec_helper' - -describe API::Issues do - set(:user) { create(:user) } - set(:project) do - create(:project, :public, creator_id: user.id, namespace: user.namespace) - end - - let(:user2) { create(:user) } - let(:non_member) { create(:user) } - set(:guest) { create(:user) } - set(:author) { create(:author) } - set(:assignee) { create(:assignee) } - let(:admin) { create(:user, :admin) } - let(:issue_title) { 'foo' } - let(:issue_description) { 'closed' } - let!(:closed_issue) do - create :closed_issue, - author: user, - assignees: [user], - project: project, - state: :closed, - milestone: milestone, - created_at: generate(:past_time), - updated_at: 3.hours.ago, - closed_at: 1.hour.ago - end - let!(:confidential_issue) do - create :issue, - :confidential, - project: project, - author: author, - assignees: [assignee], - created_at: generate(:past_time), - updated_at: 2.hours.ago - end - let!(:issue) do - create :issue, - author: user, - assignees: [user], - project: project, - milestone: milestone, - created_at: generate(:past_time), - updated_at: 1.hour.ago, - title: issue_title, - description: issue_description - end - set(:label) do - create(:label, title: 'label', color: '#FFAABB', project: project) - end - let!(:label_link) { create(:label_link, label: label, target: issue) } - let(:milestone) { create(:milestone, title: '1.0.0', project: project) } - set(:empty_milestone) do - create(:milestone, title: '2.0.0', project: project) - end - let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) } - - let(:no_milestone_title) { "None" } - let(:any_milestone_title) { "Any" } - - before(:all) do - project.add_reporter(user) - project.add_guest(guest) - end - - describe "GET /issues" do - context "when unauthenticated" do - it "returns an array of all issues" do - get api("/issues"), params: { scope: 'all' } - - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - end - - it "returns authentication error without any scope" do - get api("/issues") - - expect(response).to have_http_status(401) - end - - it "returns authentication error when scope is assigned-to-me" do - get api("/issues"), params: { scope: 'assigned-to-me' } - - expect(response).to have_http_status(401) - end - - it "returns authentication error when scope is created-by-me" do - get api("/issues"), params: { scope: 'created-by-me' } - - expect(response).to have_http_status(401) - end - end - - context "when authenticated" do - it "returns an array of issues" do - get api("/issues", user) - - expect_paginated_array_response([issue.id, closed_issue.id]) - expect(json_response.first['title']).to eq(issue.title) - expect(json_response.last).to have_key('web_url') - end - - it 'returns an array of closed issues' do - get api('/issues', user), params: { state: :closed } - - expect_paginated_array_response(closed_issue.id) - end - - it 'returns an array of opened issues' do - get api('/issues', user), params: { state: :opened } - - expect_paginated_array_response(issue.id) - end - - it 'returns an array of all issues' do - get api('/issues', user), params: { state: :all } - - expect_paginated_array_response([issue.id, closed_issue.id]) - end - - it 'returns issues assigned to me' do - issue2 = create(:issue, assignees: [user2], project: project) - - get api('/issues', user2), params: { scope: 'assigned_to_me' } - - expect_paginated_array_response(issue2.id) - end - - it 'returns issues assigned to me (kebab-case)' do - issue2 = create(:issue, assignees: [user2], project: project) - - get api('/issues', user2), params: { scope: 'assigned-to-me' } - - expect_paginated_array_response(issue2.id) - end - - it 'returns issues authored by the given author id' do - issue2 = create(:issue, author: user2, project: project) - - get api('/issues', user), params: { author_id: user2.id, scope: 'all' } - - expect_paginated_array_response(issue2.id) - end - - it 'returns issues assigned to the given assignee id' do - issue2 = create(:issue, assignees: [user2], project: project) - - get api('/issues', user), params: { assignee_id: user2.id, scope: 'all' } - - expect_paginated_array_response(issue2.id) - end - - it 'returns issues authored by the given author id and assigned to the given assignee id' do - issue2 = create(:issue, author: user2, assignees: [user2], project: project) - - get api('/issues', user), params: { author_id: user2.id, assignee_id: user2.id, scope: 'all' } - - expect_paginated_array_response(issue2.id) - end - - it 'returns issues with no assignee' do - issue2 = create(:issue, author: user2, project: project) - - get api('/issues', user), params: { assignee_id: 0, scope: 'all' } - - expect_paginated_array_response(issue2.id) - end - - it 'returns issues with no assignee' do - issue2 = create(:issue, author: user2, project: project) - - get api('/issues', user), params: { assignee_id: 'None', scope: 'all' } - - expect_paginated_array_response(issue2.id) - end - - it 'returns issues with any assignee' do - # This issue without assignee should not be returned - create(:issue, author: user2, project: project) - - get api('/issues', user), params: { assignee_id: 'Any', scope: 'all' } - - expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id]) - end - - it 'returns only confidential issues' do - get api('/issues', user), params: { confidential: true, scope: 'all' } - - expect_paginated_array_response(confidential_issue.id) - end - - it 'returns only public issues' do - get api('/issues', user), params: { confidential: false } - - expect_paginated_array_response([issue.id, closed_issue.id]) - end - - it 'returns issues reacted by the authenticated user' do - issue2 = create(:issue, project: project, author: user, assignees: [user]) - create(:award_emoji, awardable: issue2, user: user2, name: 'star') - create(:award_emoji, awardable: issue, user: user2, name: 'thumbsup') - - get api('/issues', user2), params: { my_reaction_emoji: 'Any', scope: 'all' } - - expect_paginated_array_response([issue2.id, issue.id]) - end - - it 'returns issues not reacted by the authenticated user' do - issue2 = create(:issue, project: project, author: user, assignees: [user]) - create(:award_emoji, awardable: issue2, user: user2, name: 'star') - - get api('/issues', user2), params: { my_reaction_emoji: 'None', scope: 'all' } - - expect_paginated_array_response([issue.id, closed_issue.id]) - end - - it 'returns issues matching given search string for title' do - get api("/issues", user), params: { search: issue.title } - - expect_paginated_array_response(issue.id) - end - - it 'returns issues matching given search string for title and scoped in title' do - get api("/issues", user), params: { search: issue.title, in: 'title' } - - expect_paginated_array_response(issue.id) - end - - it 'returns an empty array if no issue matches given search string for title and scoped in description' do - get api("/issues", user), params: { search: issue.title, in: 'description' } - - expect_paginated_array_response([]) - end - - it 'returns issues matching given search string for description' do - get api("/issues", user), params: { search: issue.description } - - expect_paginated_array_response(issue.id) - end - - context 'filtering before a specific date' do - let!(:issue2) { create(:issue, project: project, author: user, created_at: Date.new(2000, 1, 1), updated_at: Date.new(2000, 1, 1)) } - - it 'returns issues created before a specific date' do - get api('/issues?created_before=2000-01-02T00:00:00.060Z', user) - - expect_paginated_array_response(issue2.id) - end - - it 'returns issues updated before a specific date' do - get api('/issues?updated_before=2000-01-02T00:00:00.060Z', user) - - expect_paginated_array_response(issue2.id) - end - end - - context 'filtering after a specific date' do - let!(:issue2) { create(:issue, project: project, author: user, created_at: 1.week.from_now, updated_at: 1.week.from_now) } - - it 'returns issues created after a specific date' do - get api("/issues?created_after=#{issue2.created_at}", user) - - expect_paginated_array_response(issue2.id) - end - - it 'returns issues updated after a specific date' do - get api("/issues?updated_after=#{issue2.updated_at}", user) - - expect_paginated_array_response(issue2.id) - end - end - - it 'returns an array of labeled issues' do - get api('/issues', user), params: { labels: label.title } - - expect_paginated_array_response(issue.id) - expect(json_response.first['labels']).to eq([label.title]) - end - - it 'returns an array of labeled issues with labels param as array' do - get api('/issues', user), params: { labels: [label.title] } - - expect_paginated_array_response(issue.id) - expect(json_response.first['labels']).to eq([label.title]) - end - - it 'returns an array of labeled issues when all labels matches' do - label_b = create(:label, title: 'foo', project: project) - label_c = create(:label, title: 'bar', project: project) - - create(:label_link, label: label_b, target: issue) - create(:label_link, label: label_c, target: issue) - - get api('/issues', user), params: { labels: "#{label.title},#{label_b.title},#{label_c.title}" } - - expect_paginated_array_response(issue.id) - expect(json_response.first['labels']).to eq([label_c.title, label_b.title, label.title]) - end - - it 'returns an array of labeled issues when all labels matches with labels param as array' do - label_b = create(:label, title: 'foo', project: project) - label_c = create(:label, title: 'bar', project: project) - - create(:label_link, label: label_b, target: issue) - create(:label_link, label: label_c, target: issue) - - get api('/issues', user), params: { labels: [label.title, label_b.title, label_c.title] } - - expect_paginated_array_response(issue.id) - expect(json_response.first['labels']).to eq([label_c.title, label_b.title, label.title]) - end - - it 'returns an empty array if no issue matches labels' do - get api('/issues', user), params: { labels: 'foo,bar' } - - expect_paginated_array_response([]) - end - - it 'returns an empty array if no issue matches labels with labels param as array' do - get api('/issues', user), params: { labels: %w(foo bar) } - - expect_paginated_array_response([]) - end - - it 'returns an array of labeled issues matching given state' do - get api('/issues', user), params: { labels: label.title, state: :opened } - - expect_paginated_array_response(issue.id) - expect(json_response.first['labels']).to eq([label.title]) - expect(json_response.first['state']).to eq('opened') - end - - it 'returns an array of labeled issues matching given state with labels param as array' do - get api('/issues', user), params: { labels: [label.title], state: :opened } - - expect_paginated_array_response(issue.id) - expect(json_response.first['labels']).to eq([label.title]) - expect(json_response.first['state']).to eq('opened') - end - - it 'returns an empty array if no issue matches labels and state filters' do - get api('/issues', user), params: { labels: label.title, state: :closed } - - expect_paginated_array_response([]) - end - - it 'returns an array of issues with any label' do - get api('/issues', user), params: { labels: IssuesFinder::FILTER_ANY } - - expect_paginated_array_response(issue.id) - end - - it 'returns an array of issues with any label with labels param as array' do - get api('/issues', user), params: { labels: [IssuesFinder::FILTER_ANY] } - - expect_paginated_array_response(issue.id) - end - - it 'returns an array of issues with no label' do - get api('/issues', user), params: { labels: IssuesFinder::FILTER_NONE } - - expect_paginated_array_response(closed_issue.id) - end - - it 'returns an array of issues with no label with labels param as array' do - get api('/issues', user), params: { labels: [IssuesFinder::FILTER_NONE] } - - expect_paginated_array_response(closed_issue.id) - end - - it 'returns an array of issues with no label when using the legacy No+Label filter' do - get api('/issues', user), params: { labels: 'No Label' } - - expect_paginated_array_response(closed_issue.id) - end - - it 'returns an array of issues with no label when using the legacy No+Label filter with labels param as array' do - get api('/issues', user), params: { labels: ['No Label'] } - - expect_paginated_array_response(closed_issue.id) - end - - it 'returns an empty array if no issue matches milestone' do - get api("/issues?milestone=#{empty_milestone.title}", user) - - expect_paginated_array_response([]) - end - - it 'returns an empty array if milestone does not exist' do - get api("/issues?milestone=foo", user) - - expect_paginated_array_response([]) - end - - it 'returns an array of issues in given milestone' do - get api("/issues?milestone=#{milestone.title}", user) - - expect_paginated_array_response([issue.id, closed_issue.id]) - end - - it 'returns an array of issues matching state in milestone' do - get api("/issues?milestone=#{milestone.title}"\ - '&state=closed', user) - - expect_paginated_array_response(closed_issue.id) - end - - it 'returns an array of issues with no milestone' do - get api("/issues?milestone=#{no_milestone_title}", author) - - expect_paginated_array_response(confidential_issue.id) - end - - it 'returns an array of issues found by iids' do - get api('/issues', user), params: { iids: [closed_issue.iid] } - - expect_paginated_array_response(closed_issue.id) - end - - it 'returns an empty array if iid does not exist' do - get api("/issues", user), params: { iids: [0] } - - expect_paginated_array_response([]) - end - - context 'without sort params' do - it 'sorts by created_at descending by default' do - get api('/issues', user) - - expect_paginated_array_response([issue.id, closed_issue.id]) - end - - context 'with 2 issues with same created_at' do - let!(:closed_issue2) do - create :closed_issue, - author: user, - assignees: [user], - project: project, - milestone: milestone, - created_at: closed_issue.created_at, - updated_at: 1.hour.ago, - title: issue_title, - description: issue_description - end - - it 'page breaks first page correctly' do - get api('/issues?per_page=2', user) - - expect_paginated_array_response([issue.id, closed_issue2.id]) - end - - it 'page breaks second page correctly' do - get api('/issues?per_page=2&page=2', user) - - expect_paginated_array_response([closed_issue.id]) - end - end - end - - it 'sorts ascending when requested' do - get api('/issues?sort=asc', user) - - expect_paginated_array_response([closed_issue.id, issue.id]) - end - - it 'sorts by updated_at descending when requested' do - get api('/issues?order_by=updated_at', user) - - issue.touch(:updated_at) - - expect_paginated_array_response([issue.id, closed_issue.id]) - end - - it 'sorts by updated_at ascending when requested' do - get api('/issues?order_by=updated_at&sort=asc', user) - - issue.touch(:updated_at) - - expect_paginated_array_response([closed_issue.id, issue.id]) - end - - it 'matches V4 response schema' do - get api('/issues', user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to match_response_schema('public_api/v4/issues') - end - - it 'returns a related merge request count of 0 if there are no related merge requests' do - get api('/issues', user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to match_response_schema('public_api/v4/issues') - expect(json_response.first).to include('merge_requests_count' => 0) - end - - it 'returns a related merge request count > 0 if there are related merge requests' do - create(:merge_requests_closing_issues, issue: issue) - - get api('/issues', user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to match_response_schema('public_api/v4/issues') - expect(json_response.first).to include('merge_requests_count' => 1) - end - end - end - - describe "GET /groups/:id/issues" do - let!(:group) { create(:group) } - let!(:group_project) { create(:project, :public, creator_id: user.id, namespace: group) } - let!(:group_closed_issue) do - create :closed_issue, - author: user, - assignees: [user], - project: group_project, - state: :closed, - milestone: group_milestone, - updated_at: 3.hours.ago, - created_at: 1.day.ago - end - let!(:group_confidential_issue) do - create :issue, - :confidential, - project: group_project, - author: author, - assignees: [assignee], - updated_at: 2.hours.ago, - created_at: 2.days.ago - end - let!(:group_issue) do - create :issue, - author: user, - assignees: [user], - project: group_project, - milestone: group_milestone, - updated_at: 1.hour.ago, - title: issue_title, - description: issue_description, - created_at: 5.days.ago - end - let!(:group_label) do - create(:label, title: 'group_lbl', color: '#FFAABB', project: group_project) - end - let!(:group_label_link) { create(:label_link, label: group_label, target: group_issue) } - let!(:group_milestone) { create(:milestone, title: '3.0.0', project: group_project) } - let!(:group_empty_milestone) do - create(:milestone, title: '4.0.0', project: group_project) - end - let!(:group_note) { create(:note_on_issue, author: user, project: group_project, noteable: group_issue) } - - let(:base_url) { "/groups/#{group.id}/issues" } - - context 'when group has subgroups', :nested_groups do - let(:subgroup_1) { create(:group, parent: group) } - let(:subgroup_2) { create(:group, parent: subgroup_1) } - - let(:subgroup_1_project) { create(:project, namespace: subgroup_1) } - let(:subgroup_2_project) { create(:project, namespace: subgroup_2) } - - let!(:issue_1) { create(:issue, project: subgroup_1_project) } - let!(:issue_2) { create(:issue, project: subgroup_2_project) } - - before do - group.add_developer(user) - end - - it 'also returns subgroups projects issues' do - get api(base_url, user) - - expect_paginated_array_response([issue_2.id, issue_1.id, group_closed_issue.id, group_confidential_issue.id, group_issue.id]) - end - end - - context 'when user is unauthenticated' do - it 'lists all issues in public projects' do - get api(base_url) - - expect_paginated_array_response([group_closed_issue.id, group_issue.id]) - end - end - - context 'when user is a group member' do - before do - group_project.add_reporter(user) - end - - it 'returns all group issues (including opened and closed)' do - get api(base_url, admin) - - expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id]) - end - - it 'returns group issues without confidential issues for non project members' do - get api(base_url, non_member), params: { state: :opened } - - expect_paginated_array_response(group_issue.id) - end - - it 'returns group confidential issues for author' do - get api(base_url, author), params: { state: :opened } - - expect_paginated_array_response([group_confidential_issue.id, group_issue.id]) - end - - it 'returns group confidential issues for assignee' do - get api(base_url, assignee), params: { state: :opened } - - expect_paginated_array_response([group_confidential_issue.id, group_issue.id]) - end - - it 'returns group issues with confidential issues for project members' do - get api(base_url, user), params: { state: :opened } - - expect_paginated_array_response([group_confidential_issue.id, group_issue.id]) - end - - it 'returns group confidential issues for admin' do - get api(base_url, admin), params: { state: :opened } - - expect_paginated_array_response([group_confidential_issue.id, group_issue.id]) - end - - it 'returns only confidential issues' do - get api(base_url, user), params: { confidential: true } - - expect_paginated_array_response(group_confidential_issue.id) - end - - it 'returns only public issues' do - get api(base_url, user), params: { confidential: false } - - expect_paginated_array_response([group_closed_issue.id, group_issue.id]) - end - - it 'returns an array of labeled group issues' do - get api(base_url, user), params: { labels: group_label.title } - - expect_paginated_array_response(group_issue.id) - expect(json_response.first['labels']).to eq([group_label.title]) - end - - it 'returns an array of labeled group issues with labels param as array' do - get api(base_url, user), params: { labels: [group_label.title] } - - expect_paginated_array_response(group_issue.id) - expect(json_response.first['labels']).to eq([group_label.title]) - end - - it 'returns an array of labeled group issues where all labels match' do - get api(base_url, user), params: { labels: "#{group_label.title},foo,bar" } - - expect_paginated_array_response([]) - end - - it 'returns an array of labeled group issues where all labels match with labels param as array' do - get api(base_url, user), params: { labels: [group_label.title, 'foo', 'bar'] } - - expect_paginated_array_response([]) - end - - it 'returns issues matching given search string for title' do - get api(base_url, user), params: { search: group_issue.title } - - expect_paginated_array_response(group_issue.id) - end - - it 'returns issues matching given search string for description' do - get api(base_url, user), params: { search: group_issue.description } - - expect_paginated_array_response(group_issue.id) - end - - it 'returns an array of labeled issues when all labels matches' do - label_b = create(:label, title: 'foo', project: group_project) - label_c = create(:label, title: 'bar', project: group_project) - - create(:label_link, label: label_b, target: group_issue) - create(:label_link, label: label_c, target: group_issue) - - get api(base_url, user), params: { labels: "#{group_label.title},#{label_b.title},#{label_c.title}" } - - expect_paginated_array_response(group_issue.id) - expect(json_response.first['labels']).to eq([label_c.title, label_b.title, group_label.title]) - end - - it 'returns an array of labeled issues when all labels matches with labels param as array' do - label_b = create(:label, title: 'foo', project: group_project) - label_c = create(:label, title: 'bar', project: group_project) - - create(:label_link, label: label_b, target: group_issue) - create(:label_link, label: label_c, target: group_issue) - - get api(base_url, user), params: { labels: [group_label.title, label_b.title, label_c.title] } - - expect_paginated_array_response(group_issue.id) - expect(json_response.first['labels']).to eq([label_c.title, label_b.title, group_label.title]) - end - - it 'returns an array of issues found by iids' do - get api(base_url, user), params: { iids: [group_issue.iid] } - - expect_paginated_array_response(group_issue.id) - expect(json_response.first['id']).to eq(group_issue.id) - end - - it 'returns an empty array if iid does not exist' do - get api(base_url, user), params: { iids: [0] } - - expect_paginated_array_response([]) - end - - it 'returns an empty array if no group issue matches labels' do - get api(base_url, user), params: { labels: 'foo,bar' } - - expect_paginated_array_response([]) - end - - it 'returns an array of group issues with any label' do - get api(base_url, user), params: { labels: IssuesFinder::FILTER_ANY } - - expect_paginated_array_response(group_issue.id) - expect(json_response.first['id']).to eq(group_issue.id) - end - - it 'returns an array of group issues with any label with labels param as array' do - get api(base_url, user), params: { labels: [IssuesFinder::FILTER_ANY] } - - expect_paginated_array_response(group_issue.id) - expect(json_response.first['id']).to eq(group_issue.id) - end - - it 'returns an array of group issues with no label' do - get api(base_url, user), params: { labels: IssuesFinder::FILTER_NONE } - - expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id]) - end - - it 'returns an array of group issues with no label with labels param as array' do - get api(base_url, user), params: { labels: [IssuesFinder::FILTER_NONE] } - - expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id]) - end - - it 'returns an empty array if no issue matches milestone' do - get api(base_url, user), params: { milestone: group_empty_milestone.title } - - expect_paginated_array_response([]) - end - - it 'returns an empty array if milestone does not exist' do - get api(base_url, user), params: { milestone: 'foo' } - - expect_paginated_array_response([]) - end - - it 'returns an array of issues in given milestone' do - get api(base_url, user), params: { state: :opened, milestone: group_milestone.title } - - expect_paginated_array_response(group_issue.id) - end - - it 'returns an array of issues matching state in milestone' do - get api(base_url, user), params: { milestone: group_milestone.title, state: :closed } - - expect_paginated_array_response(group_closed_issue.id) - end - - it 'returns an array of issues with no milestone' do - get api(base_url, user), params: { milestone: no_milestone_title } - - expect(response).to have_gitlab_http_status(200) - - expect_paginated_array_response(group_confidential_issue.id) - end - - context 'without sort params' do - it 'sorts by created_at descending by default' do - get api(base_url, user) - - expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id]) - end - - context 'with 2 issues with same created_at' do - let!(:group_issue2) do - create :issue, - author: user, - assignees: [user], - project: group_project, - milestone: group_milestone, - updated_at: 1.hour.ago, - title: issue_title, - description: issue_description, - created_at: group_issue.created_at - end - - it 'page breaks first page correctly' do - get api("#{base_url}?per_page=3", user) - - expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue2.id]) - end - - it 'page breaks second page correctly' do - get api("#{base_url}?per_page=3&page=2", user) - - expect_paginated_array_response([group_issue.id]) - end - end - end - - it 'sorts ascending when requested' do - get api("#{base_url}?sort=asc", user) - - expect_paginated_array_response([group_issue.id, group_confidential_issue.id, group_closed_issue.id]) - end - - it 'sorts by updated_at descending when requested' do - get api("#{base_url}?order_by=updated_at", user) - - group_issue.touch(:updated_at) - - expect_paginated_array_response([group_issue.id, group_confidential_issue.id, group_closed_issue.id]) - end - - it 'sorts by updated_at ascending when requested' do - get api(base_url, user), params: { order_by: :updated_at, sort: :asc } - - expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id]) - end - end - end - - describe "GET /projects/:id/issues" do - let(:base_url) { "/projects/#{project.id}" } - - context 'when unauthenticated' do - it 'returns public project issues' do - get api("/projects/#{project.id}/issues") - - expect_paginated_array_response([issue.id, closed_issue.id]) - end - end - - it 'avoids N+1 queries' do - get api("/projects/#{project.id}/issues", user) - - control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do - get api("/projects/#{project.id}/issues", user) - end.count - - create_list(:issue, 3, project: project) - - expect do - get api("/projects/#{project.id}/issues", user) - end.not_to exceed_all_query_limit(control_count) - end - - it 'returns 404 when project does not exist' do - get api('/projects/1000/issues', non_member) - - expect(response).to have_gitlab_http_status(404) - end - - it "returns 404 on private projects for other users" do - private_project = create(:project, :private) - create(:issue, project: private_project) - - get api("/projects/#{private_project.id}/issues", non_member) - - expect(response).to have_gitlab_http_status(404) - end - - it 'returns no issues when user has access to project but not issues' do - restricted_project = create(:project, :public, :issues_private) - create(:issue, project: restricted_project) - - get api("/projects/#{restricted_project.id}/issues", non_member) - - expect_paginated_array_response([]) - end - - it 'returns project issues without confidential issues for non project members' do - get api("#{base_url}/issues", non_member) - - expect_paginated_array_response([issue.id, closed_issue.id]) - end - - it 'returns project issues without confidential issues for project members with guest role' do - get api("#{base_url}/issues", guest) - - expect_paginated_array_response([issue.id, closed_issue.id]) - end - - it 'returns project confidential issues for author' do - get api("#{base_url}/issues", author) - - expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id]) - end - - it 'returns only confidential issues' do - get api("#{base_url}/issues", author), params: { confidential: true } - - expect_paginated_array_response(confidential_issue.id) - end - - it 'returns only public issues' do - get api("#{base_url}/issues", author), params: { confidential: false } - - expect_paginated_array_response([issue.id, closed_issue.id]) - end - - it 'returns project confidential issues for assignee' do - get api("#{base_url}/issues", assignee) - - expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id]) - end - - it 'returns project issues with confidential issues for project members' do - get api("#{base_url}/issues", user) - - expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id]) - end - - it 'returns project confidential issues for admin' do - get api("#{base_url}/issues", admin) - - expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id]) - end - - it 'returns an array of labeled project issues' do - get api("#{base_url}/issues", user), params: { labels: label.title } - - expect_paginated_array_response(issue.id) - end - - it 'returns an array of labeled project issues with labels param as array' do - get api("#{base_url}/issues", user), params: { labels: [label.title] } - - expect_paginated_array_response(issue.id) - end - - it 'returns an array of labeled issues when all labels matches' do - label_b = create(:label, title: 'foo', project: project) - label_c = create(:label, title: 'bar', project: project) - - create(:label_link, label: label_b, target: issue) - create(:label_link, label: label_c, target: issue) - - get api("#{base_url}/issues", user), params: { labels: "#{label.title},#{label_b.title},#{label_c.title}" } - - expect_paginated_array_response(issue.id) - end - - it 'returns an array of labeled issues when all labels matches with labels param as array' do - label_b = create(:label, title: 'foo', project: project) - label_c = create(:label, title: 'bar', project: project) - - create(:label_link, label: label_b, target: issue) - create(:label_link, label: label_c, target: issue) - - get api("#{base_url}/issues", user), params: { labels: [label.title, label_b.title, label_c.title] } - - expect_paginated_array_response(issue.id) - end - - it 'returns issues matching given search string for title' do - get api("#{base_url}/issues?search=#{issue.title}", user) - - expect_paginated_array_response(issue.id) - end - - it 'returns issues matching given search string for description' do - get api("#{base_url}/issues?search=#{issue.description}", user) - - expect_paginated_array_response(issue.id) - end - - it 'returns an array of issues found by iids' do - get api("#{base_url}/issues", user), params: { iids: [issue.iid] } - - expect_paginated_array_response(issue.id) - end - - it 'returns an empty array if iid does not exist' do - get api("#{base_url}/issues", user), params: { iids: [0] } - - expect_paginated_array_response([]) - end - - it 'returns an empty array if not all labels matches' do - get api("#{base_url}/issues?labels=#{label.title},foo", user) - - expect_paginated_array_response([]) - end - - it 'returns an array of project issues with any label' do - get api("#{base_url}/issues", user), params: { labels: IssuesFinder::FILTER_ANY } - - expect_paginated_array_response(issue.id) - end - - it 'returns an array of project issues with any label with labels param as array' do - get api("#{base_url}/issues", user), params: { labels: [IssuesFinder::FILTER_ANY] } - - expect_paginated_array_response(issue.id) - end - - it 'returns an array of project issues with no label' do - get api("#{base_url}/issues", user), params: { labels: IssuesFinder::FILTER_NONE } - - expect_paginated_array_response([confidential_issue.id, closed_issue.id]) - end - - it 'returns an array of project issues with no label with labels param as array' do - get api("#{base_url}/issues", user), params: { labels: [IssuesFinder::FILTER_NONE] } - - expect_paginated_array_response([confidential_issue.id, closed_issue.id]) - end - - it 'returns an empty array if no project issue matches labels' do - get api("#{base_url}/issues", user), params: { labels: 'foo,bar' } - - expect_paginated_array_response([]) - end - - it 'returns an empty array if no issue matches milestone' do - get api("#{base_url}/issues", user), params: { milestone: empty_milestone.title } - - expect_paginated_array_response([]) - end - - it 'returns an empty array if milestone does not exist' do - get api("#{base_url}/issues", user), params: { milestone: :foo } - - expect_paginated_array_response([]) - end - - it 'returns an array of issues in given milestone' do - get api("#{base_url}/issues", user), params: { milestone: milestone.title } - - expect_paginated_array_response([issue.id, closed_issue.id]) - end - - it 'returns an array of issues matching state in milestone' do - get api("#{base_url}/issues", user), params: { milestone: milestone.title, state: :closed } - - expect_paginated_array_response(closed_issue.id) - end - - it 'returns an array of issues with no milestone' do - get api("#{base_url}/issues", user), params: { milestone: no_milestone_title } - - expect_paginated_array_response(confidential_issue.id) - end - - it 'returns an array of issues with any milestone' do - get api("#{base_url}/issues", user), params: { milestone: any_milestone_title } - - expect_paginated_array_response([issue.id, closed_issue.id]) - end - - context 'without sort params' do - it 'sorts by created_at descending by default' do - get api("#{base_url}/issues", user) - - expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id]) - end - - context 'with 2 issues with same created_at' do - let!(:closed_issue2) do - create :closed_issue, - author: user, - assignees: [user], - project: project, - milestone: milestone, - created_at: closed_issue.created_at, - updated_at: 1.hour.ago, - title: issue_title, - description: issue_description - end - - it 'page breaks first page correctly' do - get api("#{base_url}/issues?per_page=3", user) - - expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue2.id]) - end - - it 'page breaks second page correctly' do - get api("#{base_url}/issues?per_page=3&page=2", user) - - expect_paginated_array_response([closed_issue.id]) - end - end - end - - it 'sorts ascending when requested' do - get api("#{base_url}/issues", user), params: { sort: :asc } - - expect_paginated_array_response([closed_issue.id, confidential_issue.id, issue.id]) - end - - it 'sorts by updated_at descending when requested' do - get api("#{base_url}/issues", user), params: { order_by: :updated_at } - - issue.touch(:updated_at) - - expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id]) - end - - it 'sorts by updated_at ascending when requested' do - get api("#{base_url}/issues", user), params: { order_by: :updated_at, sort: :asc } - - expect_paginated_array_response([closed_issue.id, confidential_issue.id, issue.id]) - end - end - - describe "GET /projects/:id/issues/:issue_iid" do - context 'when unauthenticated' do - it 'returns public issues' do - get api("/projects/#{project.id}/issues/#{issue.iid}") - - expect(response).to have_gitlab_http_status(200) - end - end - - it 'exposes known attributes' do - get api("/projects/#{project.id}/issues/#{issue.iid}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['id']).to eq(issue.id) - expect(json_response['iid']).to eq(issue.iid) - expect(json_response['project_id']).to eq(issue.project.id) - expect(json_response['title']).to eq(issue.title) - expect(json_response['description']).to eq(issue.description) - expect(json_response['state']).to eq(issue.state) - expect(json_response['closed_at']).to be_falsy - expect(json_response['created_at']).to be_present - expect(json_response['updated_at']).to be_present - expect(json_response['labels']).to eq(issue.label_names) - expect(json_response['milestone']).to be_a Hash - expect(json_response['assignees']).to be_a Array - expect(json_response['assignee']).to be_a Hash - expect(json_response['author']).to be_a Hash - expect(json_response['confidential']).to be_falsy - end - - it "exposes the 'closed_at' attribute" do - get api("/projects/#{project.id}/issues/#{closed_issue.iid}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['closed_at']).to be_present - end - - context 'links exposure' do - it 'exposes related resources full URIs' do - get api("/projects/#{project.id}/issues/#{issue.iid}", user) - - links = json_response['_links'] - - expect(links['self']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}") - expect(links['notes']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}/notes") - expect(links['award_emoji']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}/award_emoji") - expect(links['project']).to end_with("/api/v4/projects/#{project.id}") - end - end - - it "returns a project issue by internal id" do - get api("/projects/#{project.id}/issues/#{issue.iid}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq(issue.title) - expect(json_response['iid']).to eq(issue.iid) - end - - it "returns 404 if issue id not found" do - get api("/projects/#{project.id}/issues/54321", user) - expect(response).to have_gitlab_http_status(404) - end - - it "returns 404 if the issue ID is used" do - get api("/projects/#{project.id}/issues/#{issue.id}", user) - - expect(response).to have_gitlab_http_status(404) - end - - context 'confidential issues' do - it "returns 404 for non project members" do - get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member) - - expect(response).to have_gitlab_http_status(404) - end - - it "returns 404 for project members with guest role" do - get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest) - - expect(response).to have_gitlab_http_status(404) - end - - it "returns confidential issue for project members" do - get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq(confidential_issue.title) - expect(json_response['iid']).to eq(confidential_issue.iid) - end - - it "returns confidential issue for author" do - get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq(confidential_issue.title) - expect(json_response['iid']).to eq(confidential_issue.iid) - end - - it "returns confidential issue for assignee" do - get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", assignee) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq(confidential_issue.title) - expect(json_response['iid']).to eq(confidential_issue.iid) - end - - it "returns confidential issue for admin" do - get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq(confidential_issue.title) - expect(json_response['iid']).to eq(confidential_issue.iid) - end - end - end - - describe "POST /projects/:id/issues" do - context 'support for deprecated assignee_id' do - it 'creates a new project issue' do - post api("/projects/#{project.id}/issues", user), - params: { title: 'new issue', assignee_id: user2.id } - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('new issue') - expect(json_response['assignee']['name']).to eq(user2.name) - expect(json_response['assignees'].first['name']).to eq(user2.name) - end - - it 'creates a new project issue when assignee_id is empty' do - post api("/projects/#{project.id}/issues", user), - params: { title: 'new issue', assignee_id: '' } - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('new issue') - expect(json_response['assignee']).to be_nil - end - end - - context 'single assignee restrictions' do - it 'creates a new project issue with no more than one assignee' do - post api("/projects/#{project.id}/issues", user), - params: { title: 'new issue', assignee_ids: [user2.id, guest.id] } - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('new issue') - expect(json_response['assignees'].count).to eq(1) - end - end - - context 'user does not have permissions to create issue' do - let(:not_member) { create(:user) } - - before do - project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE) - end - - it 'renders 403' do - post api("/projects/#{project.id}/issues", not_member), params: { title: 'new issue' } - - expect(response).to have_gitlab_http_status(403) - end - end - - context 'an internal ID is provided' do - context 'by an admin' do - it 'sets the internal ID on the new issue' do - post api("/projects/#{project.id}/issues", admin), - params: { title: 'new issue', iid: 9001 } - - expect(response).to have_gitlab_http_status(201) - expect(json_response['iid']).to eq 9001 - end - end - - context 'by an owner' do - it 'sets the internal ID on the new issue' do - post api("/projects/#{project.id}/issues", user), - params: { title: 'new issue', iid: 9001 } - - expect(response).to have_gitlab_http_status(201) - expect(json_response['iid']).to eq 9001 - end - end - - context 'by a group owner' do - let(:group) { create(:group) } - let(:group_project) { create(:project, :public, namespace: group) } - - it 'sets the internal ID on the new issue' do - group.add_owner(user2) - post api("/projects/#{group_project.id}/issues", user2), - params: { title: 'new issue', iid: 9001 } - - expect(response).to have_gitlab_http_status(201) - expect(json_response['iid']).to eq 9001 - end - end - - context 'by another user' do - it 'ignores the given internal ID' do - post api("/projects/#{project.id}/issues", user2), - params: { title: 'new issue', iid: 9001 } - - expect(response).to have_gitlab_http_status(201) - expect(json_response['iid']).not_to eq 9001 - end - end - end - - it 'creates a new project issue' do - post api("/projects/#{project.id}/issues", user), - params: { title: 'new issue', labels: 'label, label2', weight: 3, assignee_ids: [user2.id] } - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('new issue') - expect(json_response['description']).to be_nil - expect(json_response['labels']).to eq(%w(label label2)) - expect(json_response['confidential']).to be_falsy - expect(json_response['assignee']['name']).to eq(user2.name) - expect(json_response['assignees'].first['name']).to eq(user2.name) - end - - it 'creates a new project issue with labels param as array' do - post api("/projects/#{project.id}/issues", user), - params: { title: 'new issue', labels: %w(label label2), weight: 3, assignee_ids: [user2.id] } - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('new issue') - expect(json_response['description']).to be_nil - expect(json_response['labels']).to eq(%w(label label2)) - expect(json_response['confidential']).to be_falsy - expect(json_response['assignee']['name']).to eq(user2.name) - expect(json_response['assignees'].first['name']).to eq(user2.name) - end - - it 'creates a new confidential project issue' do - post api("/projects/#{project.id}/issues", user), - params: { title: 'new issue', confidential: true } - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('new issue') - expect(json_response['confidential']).to be_truthy - end - - it 'creates a new confidential project issue with a different param' do - post api("/projects/#{project.id}/issues", user), - params: { title: 'new issue', confidential: 'y' } - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('new issue') - expect(json_response['confidential']).to be_truthy - end - - it 'creates a public issue when confidential param is false' do - post api("/projects/#{project.id}/issues", user), - params: { title: 'new issue', confidential: false } - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('new issue') - expect(json_response['confidential']).to be_falsy - end - - it 'creates a public issue when confidential param is invalid' do - post api("/projects/#{project.id}/issues", user), - params: { title: 'new issue', confidential: 'foo' } - - expect(response).to have_gitlab_http_status(400) - expect(json_response['error']).to eq('confidential is invalid') - end - - it "returns a 400 bad request if title not given" do - post api("/projects/#{project.id}/issues", user), params: { labels: 'label, label2' } - expect(response).to have_gitlab_http_status(400) - end - - it 'allows special label names' do - post api("/projects/#{project.id}/issues", user), - params: { - title: 'new issue', - labels: 'label, label?, label&foo, ?, &' - } - expect(response.status).to eq(201) - expect(json_response['labels']).to include 'label' - expect(json_response['labels']).to include 'label?' - expect(json_response['labels']).to include 'label&foo' - expect(json_response['labels']).to include '?' - expect(json_response['labels']).to include '&' - end - - it 'allows special label names with labels param as array' do - post api("/projects/#{project.id}/issues", user), - params: { - title: 'new issue', - labels: ['label', 'label?', 'label&foo, ?, &'] - } - expect(response.status).to eq(201) - expect(json_response['labels']).to include 'label' - expect(json_response['labels']).to include 'label?' - expect(json_response['labels']).to include 'label&foo' - expect(json_response['labels']).to include '?' - expect(json_response['labels']).to include '&' - end - - it 'returns 400 if title is too long' do - post api("/projects/#{project.id}/issues", user), - params: { title: 'g' * 256 } - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']['title']).to eq([ - 'is too long (maximum is 255 characters)' - ]) - end - - context 'resolving discussions' do - let(:discussion) { create(:diff_note_on_merge_request).to_discussion } - let(:merge_request) { discussion.noteable } - let(:project) { merge_request.source_project } - - before do - project.add_maintainer(user) - end - - context 'resolving all discussions in a merge request' do - before do - post api("/projects/#{project.id}/issues", user), - params: { - title: 'New Issue', - merge_request_to_resolve_discussions_of: merge_request.iid - } - end - - it_behaves_like 'creating an issue resolving discussions through the API' - end - - context 'resolving a single discussion' do - before do - post api("/projects/#{project.id}/issues", user), - params: { - title: 'New Issue', - merge_request_to_resolve_discussions_of: merge_request.iid, - discussion_to_resolve: discussion.id - } - end - - it_behaves_like 'creating an issue resolving discussions through the API' - end - end - - context 'with due date' do - it 'creates a new project issue' do - due_date = 2.weeks.from_now.strftime('%Y-%m-%d') - - post api("/projects/#{project.id}/issues", user), - params: { title: 'new issue', due_date: due_date } - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('new issue') - expect(json_response['description']).to be_nil - expect(json_response['due_date']).to eq(due_date) - end - end - - context 'setting created_at' do - let(:creation_time) { 2.weeks.ago } - let(:params) { { title: 'new issue', labels: 'label, label2', created_at: creation_time } } - - context 'by an admin' do - before do - post api("/projects/#{project.id}/issues", admin), params: params - end - - it 'sets the creation time on the new issue' do - expect(response).to have_gitlab_http_status(201) - expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time) - end - - it 'sets the system notes timestamp based on creation time' do - issue = Issue.find(json_response['id']) - - expect(issue.resource_label_events.last.created_at).to be_like_time(creation_time) - end - end - - context 'by a project owner' do - it 'sets the creation time on the new issue' do - post api("/projects/#{project.id}/issues", user), params: params - - expect(response).to have_gitlab_http_status(201) - expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time) - end - end - - context 'by a group owner' do - it 'sets the creation time on the new issue' do - group = create(:group) - group_project = create(:project, :public, namespace: group) - group.add_owner(user2) - post api("/projects/#{group_project.id}/issues", user2), params: params - - expect(response).to have_gitlab_http_status(201) - expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time) - end - end - - context 'by another user' do - it 'ignores the given creation time' do - post api("/projects/#{project.id}/issues", user2), params: params - - expect(response).to have_gitlab_http_status(201) - expect(Time.parse(json_response['created_at'])).not_to be_like_time(creation_time) - end - end - end - - context 'the user can only read the issue' do - it 'cannot create new labels' do - expect do - post api("/projects/#{project.id}/issues", non_member), params: { title: 'new issue', labels: 'label, label2' } - end.not_to change { project.labels.count } - end - - it 'cannot create new labels with labels param as array' do - expect do - post api("/projects/#{project.id}/issues", non_member), params: { title: 'new issue', labels: %w(label label2) } - end.not_to change { project.labels.count } - end - end - end - - describe 'POST /projects/:id/issues with spam filtering' do - before do - allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true) - allow_any_instance_of(AkismetService).to receive_messages(spam?: true) - end - - let(:params) do - { - title: 'new issue', - description: 'content here', - labels: 'label, label2' - } - end - - it "does not create a new project issue" do - expect { post api("/projects/#{project.id}/issues", user), params: params }.not_to change(Issue, :count) - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']).to eq({ "error" => "Spam detected" }) - - spam_logs = SpamLog.all - expect(spam_logs.count).to eq(1) - expect(spam_logs[0].title).to eq('new issue') - expect(spam_logs[0].description).to eq('content here') - expect(spam_logs[0].user).to eq(user) - expect(spam_logs[0].noteable_type).to eq('Issue') - end - end - - describe "PUT /projects/:id/issues/:issue_iid to update only title" do - it "updates a project issue" do - put api("/projects/#{project.id}/issues/#{issue.iid}", user), - params: { title: 'updated title' } - expect(response).to have_gitlab_http_status(200) - - expect(json_response['title']).to eq('updated title') - end - - it "returns 404 error if issue iid not found" do - put api("/projects/#{project.id}/issues/44444", user), - params: { title: 'updated title' } - expect(response).to have_gitlab_http_status(404) - end - - it "returns 404 error if issue id is used instead of the iid" do - put api("/projects/#{project.id}/issues/#{issue.id}", user), - params: { title: 'updated title' } - expect(response).to have_gitlab_http_status(404) - end - - it 'allows special label names' do - put api("/projects/#{project.id}/issues/#{issue.iid}", user), - params: { - title: 'updated title', - labels: 'label, label?, label&foo, ?, &' - } - - expect(response.status).to eq(200) - expect(json_response['labels']).to include 'label' - expect(json_response['labels']).to include 'label?' - expect(json_response['labels']).to include 'label&foo' - expect(json_response['labels']).to include '?' - expect(json_response['labels']).to include '&' - end - - it 'allows special label names with labels param as array' do - put api("/projects/#{project.id}/issues/#{issue.iid}", user), - params: { - title: 'updated title', - labels: ['label', 'label?', 'label&foo, ?, &'] - } - - expect(response.status).to eq(200) - expect(json_response['labels']).to include 'label' - expect(json_response['labels']).to include 'label?' - expect(json_response['labels']).to include 'label&foo' - expect(json_response['labels']).to include '?' - expect(json_response['labels']).to include '&' - end - - context 'confidential issues' do - it "returns 403 for non project members" do - put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member), - params: { title: 'updated title' } - expect(response).to have_gitlab_http_status(403) - end - - it "returns 403 for project members with guest role" do - put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest), - params: { title: 'updated title' } - expect(response).to have_gitlab_http_status(403) - end - - it "updates a confidential issue for project members" do - put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user), - params: { title: 'updated title' } - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq('updated title') - end - - it "updates a confidential issue for author" do - put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author), - params: { title: 'updated title' } - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq('updated title') - end - - it "updates a confidential issue for admin" do - put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin), - params: { title: 'updated title' } - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq('updated title') - end - - it 'sets an issue to confidential' do - put api("/projects/#{project.id}/issues/#{issue.iid}", user), - params: { confidential: true } - - expect(response).to have_gitlab_http_status(200) - expect(json_response['confidential']).to be_truthy - end - - it 'makes a confidential issue public' do - put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user), - params: { confidential: false } - - expect(response).to have_gitlab_http_status(200) - expect(json_response['confidential']).to be_falsy - end - - it 'does not update a confidential issue with wrong confidential flag' do - put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user), - params: { confidential: 'foo' } - - expect(response).to have_gitlab_http_status(400) - expect(json_response['error']).to eq('confidential is invalid') - end - end - end - - describe 'PUT /projects/:id/issues/:issue_iid with spam filtering' do - let(:params) do - { - title: 'updated title', - description: 'content here', - labels: 'label, label2' - } - end - - it "does not create a new project issue" do - allow_any_instance_of(SpamService).to receive_messages(check_for_spam?: true) - allow_any_instance_of(AkismetService).to receive_messages(spam?: true) - - put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: params - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']).to eq({ "error" => "Spam detected" }) - - spam_logs = SpamLog.all - expect(spam_logs.count).to eq(1) - expect(spam_logs[0].title).to eq('updated title') - expect(spam_logs[0].description).to eq('content here') - expect(spam_logs[0].user).to eq(user) - expect(spam_logs[0].noteable_type).to eq('Issue') - end - end - - describe 'PUT /projects/:id/issues/:issue_iid to update assignee' do - context 'support for deprecated assignee_id' do - it 'removes assignee' do - put api("/projects/#{project.id}/issues/#{issue.iid}", user), - params: { assignee_id: 0 } - - expect(response).to have_gitlab_http_status(200) - - expect(json_response['assignee']).to be_nil - end - - it 'updates an issue with new assignee' do - put api("/projects/#{project.id}/issues/#{issue.iid}", user), - params: { assignee_id: user2.id } - - expect(response).to have_gitlab_http_status(200) - - expect(json_response['assignee']['name']).to eq(user2.name) - end - end - - it 'removes assignee' do - put api("/projects/#{project.id}/issues/#{issue.iid}", user), - params: { assignee_ids: [0] } - - expect(response).to have_gitlab_http_status(200) - - expect(json_response['assignees']).to be_empty - end - - it 'updates an issue with new assignee' do - put api("/projects/#{project.id}/issues/#{issue.iid}", user), - params: { assignee_ids: [user2.id] } - - expect(response).to have_gitlab_http_status(200) - - expect(json_response['assignees'].first['name']).to eq(user2.name) - end - - context 'single assignee restrictions' do - it 'updates an issue with several assignees but only one has been applied' do - put api("/projects/#{project.id}/issues/#{issue.iid}", user), - params: { assignee_ids: [user2.id, guest.id] } - - expect(response).to have_gitlab_http_status(200) - - expect(json_response['assignees'].size).to eq(1) - end - end - end - - describe 'PUT /projects/:id/issues/:issue_iid to update labels' do - let!(:label) { create(:label, title: 'dummy', project: project) } - let!(:label_link) { create(:label_link, label: label, target: issue) } - - it 'does not update labels if not present' do - put api("/projects/#{project.id}/issues/#{issue.iid}", user), - params: { title: 'updated title' } - expect(response).to have_gitlab_http_status(200) - expect(json_response['labels']).to eq([label.title]) - end - - it 'removes all labels and touches the record' do - Timecop.travel(1.minute.from_now) do - put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { labels: '' } - end - - expect(response).to have_gitlab_http_status(200) - expect(json_response['labels']).to eq([]) - expect(json_response['updated_at']).to be > Time.now - end - - it 'removes all labels and touches the record with labels param as array' do - Timecop.travel(1.minute.from_now) do - put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { labels: [''] } - end - - expect(response).to have_gitlab_http_status(200) - expect(json_response['labels']).to eq([]) - expect(json_response['updated_at']).to be > Time.now - end - - it 'updates labels and touches the record' do - Timecop.travel(1.minute.from_now) do - put api("/projects/#{project.id}/issues/#{issue.iid}", user), - params: { labels: 'foo,bar' } - end - expect(response).to have_gitlab_http_status(200) - expect(json_response['labels']).to include 'foo' - expect(json_response['labels']).to include 'bar' - expect(json_response['updated_at']).to be > Time.now - end - - it 'updates labels and touches the record with labels param as array' do - Timecop.travel(1.minute.from_now) do - put api("/projects/#{project.id}/issues/#{issue.iid}", user), - params: { labels: %w(foo bar) } - end - expect(response).to have_gitlab_http_status(200) - expect(json_response['labels']).to include 'foo' - expect(json_response['labels']).to include 'bar' - expect(json_response['updated_at']).to be > Time.now - end - - it 'allows special label names' do - put api("/projects/#{project.id}/issues/#{issue.iid}", user), - params: { labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&' } - expect(response.status).to eq(200) - expect(json_response['labels']).to include 'label:foo' - expect(json_response['labels']).to include 'label-bar' - expect(json_response['labels']).to include 'label_bar' - expect(json_response['labels']).to include 'label/bar' - expect(json_response['labels']).to include 'label?bar' - expect(json_response['labels']).to include 'label&bar' - expect(json_response['labels']).to include '?' - expect(json_response['labels']).to include '&' - end - - it 'allows special label names with labels param as array' do - put api("/projects/#{project.id}/issues/#{issue.iid}", user), - params: { labels: ['label:foo', 'label-bar', 'label_bar', 'label/bar,label?bar,label&bar,?,&'] } - expect(response.status).to eq(200) - expect(json_response['labels']).to include 'label:foo' - expect(json_response['labels']).to include 'label-bar' - expect(json_response['labels']).to include 'label_bar' - expect(json_response['labels']).to include 'label/bar' - expect(json_response['labels']).to include 'label?bar' - expect(json_response['labels']).to include 'label&bar' - expect(json_response['labels']).to include '?' - expect(json_response['labels']).to include '&' - end - - it 'returns 400 if title is too long' do - put api("/projects/#{project.id}/issues/#{issue.iid}", user), - params: { title: 'g' * 256 } - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']['title']).to eq([ - 'is too long (maximum is 255 characters)' - ]) - end - end - - describe "PUT /projects/:id/issues/:issue_iid to update state and label" do - it "updates a project issue" do - put api("/projects/#{project.id}/issues/#{issue.iid}", user), - params: { labels: 'label2', state_event: "close" } - expect(response).to have_gitlab_http_status(200) - - expect(json_response['labels']).to include 'label2' - expect(json_response['state']).to eq "closed" - end - - it 'reopens a project isssue' do - put api("/projects/#{project.id}/issues/#{closed_issue.iid}", user), params: { state_event: 'reopen' } - - expect(response).to have_gitlab_http_status(200) - expect(json_response['state']).to eq 'opened' - end - - context 'when an admin or owner makes the request' do - it 'accepts the update date to be set' do - update_time = 2.weeks.ago - put api("/projects/#{project.id}/issues/#{issue.iid}", user), - params: { labels: 'label3', state_event: 'close', updated_at: update_time } - - expect(response).to have_gitlab_http_status(200) - expect(json_response['labels']).to include 'label3' - expect(Time.parse(json_response['updated_at'])).to be_like_time(update_time) - end - end - end - - describe 'PUT /projects/:id/issues/:issue_iid to update due date' do - it 'creates a new project issue' do - due_date = 2.weeks.from_now.strftime('%Y-%m-%d') - - put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { due_date: due_date } - - expect(response).to have_gitlab_http_status(200) - expect(json_response['due_date']).to eq(due_date) - end - end - - describe "DELETE /projects/:id/issues/:issue_iid" do - it "rejects a non member from deleting an issue" do - delete api("/projects/#{project.id}/issues/#{issue.iid}", non_member) - expect(response).to have_gitlab_http_status(403) - end - - it "rejects a developer from deleting an issue" do - delete api("/projects/#{project.id}/issues/#{issue.iid}", author) - expect(response).to have_gitlab_http_status(403) - end - - context "when the user is project owner" do - let(:owner) { create(:user) } - let(:project) { create(:project, namespace: owner.namespace) } - - it "deletes the issue if an admin requests it" do - delete api("/projects/#{project.id}/issues/#{issue.iid}", owner) - - expect(response).to have_gitlab_http_status(204) - end - - it_behaves_like '412 response' do - let(:request) { api("/projects/#{project.id}/issues/#{issue.iid}", owner) } - end - end - - context 'when issue does not exist' do - it 'returns 404 when trying to move an issue' do - delete api("/projects/#{project.id}/issues/123", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - it 'returns 404 when using the issue ID instead of IID' do - delete api("/projects/#{project.id}/issues/#{issue.id}", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe '/projects/:id/issues/:issue_iid/move' do - let!(:target_project) { create(:project, creator_id: user.id, namespace: user.namespace ) } - let!(:target_project2) { create(:project, creator_id: non_member.id, namespace: non_member.namespace ) } - - it 'moves an issue' do - post api("/projects/#{project.id}/issues/#{issue.iid}/move", user), - params: { to_project_id: target_project.id } - - expect(response).to have_gitlab_http_status(201) - expect(json_response['project_id']).to eq(target_project.id) - end - - context 'when source and target projects are the same' do - it 'returns 400 when trying to move an issue' do - post api("/projects/#{project.id}/issues/#{issue.iid}/move", user), - params: { to_project_id: project.id } - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']).to eq('Cannot move issue to project it originates from!') - end - end - - context 'when the user does not have the permission to move issues' do - it 'returns 400 when trying to move an issue' do - post api("/projects/#{project.id}/issues/#{issue.iid}/move", user), - params: { to_project_id: target_project2.id } - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']).to eq('Cannot move issue due to insufficient permissions!') - end - end - - it 'moves the issue to another namespace if I am admin' do - post api("/projects/#{project.id}/issues/#{issue.iid}/move", admin), - params: { to_project_id: target_project2.id } - - expect(response).to have_gitlab_http_status(201) - expect(json_response['project_id']).to eq(target_project2.id) - end - - context 'when using the issue ID instead of iid' do - it 'returns 404 when trying to move an issue' do - post api("/projects/#{project.id}/issues/#{issue.id}/move", user), - params: { to_project_id: target_project.id } - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Issue Not Found') - end - end - - context 'when issue does not exist' do - it 'returns 404 when trying to move an issue' do - post api("/projects/#{project.id}/issues/123/move", user), - params: { to_project_id: target_project.id } - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Issue Not Found') - end - end - - context 'when source project does not exist' do - it 'returns 404 when trying to move an issue' do - post api("/projects/0/issues/#{issue.iid}/move", user), - params: { to_project_id: target_project.id } - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Project Not Found') - end - end - - context 'when target project does not exist' do - it 'returns 404 when trying to move an issue' do - post api("/projects/#{project.id}/issues/#{issue.iid}/move", user), - params: { to_project_id: 0 } - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe 'POST :id/issues/:issue_iid/subscribe' do - it 'subscribes to an issue' do - post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user2) - - expect(response).to have_gitlab_http_status(201) - expect(json_response['subscribed']).to eq(true) - end - - it 'returns 304 if already subscribed' do - post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user) - - expect(response).to have_gitlab_http_status(304) - end - - it 'returns 404 if the issue is not found' do - post api("/projects/#{project.id}/issues/123/subscribe", user) - - expect(response).to have_gitlab_http_status(404) - end - - it 'returns 404 if the issue ID is used instead of the iid' do - post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user) - - expect(response).to have_gitlab_http_status(404) - end - - it 'returns 404 if the issue is confidential' do - post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/subscribe", non_member) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'POST :id/issues/:issue_id/unsubscribe' do - it 'unsubscribes from an issue' do - post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user) - - expect(response).to have_gitlab_http_status(201) - expect(json_response['subscribed']).to eq(false) - end - - it 'returns 304 if not subscribed' do - post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user2) - - expect(response).to have_gitlab_http_status(304) - end - - it 'returns 404 if the issue is not found' do - post api("/projects/#{project.id}/issues/123/unsubscribe", user) - - expect(response).to have_gitlab_http_status(404) - end - - it 'returns 404 if using the issue ID instead of iid' do - post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user) - - expect(response).to have_gitlab_http_status(404) - end - - it 'returns 404 if the issue is confidential' do - post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/unsubscribe", non_member) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'time tracking endpoints' do - let(:issuable) { issue } - - include_examples 'time tracking endpoints', 'issue' - end - - describe 'GET :id/issues/:issue_iid/closed_by' do - let(:merge_request) do - create(:merge_request, - :simple, - author: user, - source_project: project, - target_project: project, - description: "closes #{issue.to_reference}") - end - - before do - create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request) - end - - context 'when unauthenticated' do - it 'return public project issues' do - get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by") - - expect_paginated_array_response(merge_request.id) - end - end - - it 'returns merge requests that will close issue on merge' do - get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by", user) - - expect_paginated_array_response(merge_request.id) - end - - context 'when no merge requests will close issue' do - it 'returns empty array' do - get api("/projects/#{project.id}/issues/#{closed_issue.iid}/closed_by", user) - - expect_paginated_array_response([]) - end - end - - it "returns 404 when issue doesn't exists" do - get api("/projects/#{project.id}/issues/0/closed_by", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'GET :id/issues/:issue_iid/related_merge_requests' do - def get_related_merge_requests(project_id, issue_iid, user = nil) - get api("/projects/#{project_id}/issues/#{issue_iid}/related_merge_requests", user) - end - - def create_referencing_mr(user, project, issue) - attributes = { - author: user, - source_project: project, - target_project: project, - source_branch: "master", - target_branch: "test", - description: "See #{issue.to_reference}" - } - create(:merge_request, attributes).tap do |merge_request| - create(:note, :system, project: issue.project, noteable: issue, author: user, note: merge_request.to_reference(full: true)) - end - end - - let!(:related_mr) { create_referencing_mr(user, project, issue) } - - context 'when unauthenticated' do - it 'return list of referenced merge requests from issue' do - get_related_merge_requests(project.id, issue.iid) - - expect_paginated_array_response(related_mr.id) - end - - it 'renders 404 if project is not visible' do - private_project = create(:project, :private) - private_issue = create(:issue, project: private_project) - create_referencing_mr(user, private_project, private_issue) - - get_related_merge_requests(private_project.id, private_issue.iid) - - expect(response).to have_gitlab_http_status(404) - end - end - - it 'returns merge requests that mentioned a issue' do - create(:merge_request, - :simple, - author: user, - source_project: project, - target_project: project, - description: "Some description") - - get_related_merge_requests(project.id, issue.iid, user) - - expect_paginated_array_response(related_mr.id) - end - - it 'returns merge requests cross-project wide' do - project2 = create(:project, :public, creator_id: user.id, namespace: user.namespace) - merge_request = create_referencing_mr(user, project2, issue) - - get_related_merge_requests(project.id, issue.iid, user) - - expect_paginated_array_response([related_mr.id, merge_request.id]) - end - - it 'does not generate references to projects with no access' do - private_project = create(:project, :private) - create_referencing_mr(private_project.creator, private_project, issue) - - get_related_merge_requests(project.id, issue.iid, user) - - expect_paginated_array_response(related_mr.id) - end - - context 'merge request closes an issue' do - let!(:closing_issue_mr_rel) do - create(:merge_requests_closing_issues, issue: issue, merge_request: related_mr) - end - - it 'returns closing MR only once' do - get_related_merge_requests(project.id, issue.iid, user) - - expect_paginated_array_response([related_mr.id]) - end - end - - context 'no merge request mentioned a issue' do - it 'returns empty array' do - get_related_merge_requests(project.id, closed_issue.iid, user) - - expect_paginated_array_response([]) - end - end - - it "returns 404 when issue doesn't exists" do - get_related_merge_requests(project.id, 0, user) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe "GET /projects/:id/issues/:issue_iid/user_agent_detail" do - let!(:user_agent_detail) { create(:user_agent_detail, subject: issue) } - - context 'when unauthenticated' do - it "returns unauthorized" do - get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail") - - expect(response).to have_gitlab_http_status(401) - end - end - - it 'exposes known attributes' do - get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['user_agent']).to eq(user_agent_detail.user_agent) - expect(json_response['ip_address']).to eq(user_agent_detail.ip_address) - expect(json_response['akismet_submitted']).to eq(user_agent_detail.submitted) - end - - it "returns unauthorized for non-admin users" do - get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", user) - - expect(response).to have_gitlab_http_status(403) - end - end - - describe 'GET projects/:id/issues/:issue_iid/participants' do - it_behaves_like 'issuable participants endpoint' do - let(:entity) { issue } - end - - it 'returns 404 if the issue is confidential' do - post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/participants", non_member) - - expect(response).to have_gitlab_http_status(404) - end - end -end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 5c94a87529b..007f3517e64 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -1495,6 +1495,33 @@ describe API::MergeRequests do expect(json_response['merge_when_pipeline_succeeds']).to eq(true) end + context 'when the MR requires pipeline success' do + it 'returns 405 if the pipeline is missing' do + allow_any_instance_of(MergeRequest) + .to receive(:merge_when_pipeline_succeeds).and_return(true) + allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(nil) + + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user) + + expect(response).to have_gitlab_http_status(405) + expect(json_response['message']).to eq('Not allowed: pipeline does not exist') + end + end + + context 'when the request requires pipeline success' do + it 'returns 405 if the pipeline is missing' do + allow_any_instance_of(MergeRequest) + .to receive(:merge_when_pipeline_succeeds).and_return(true) + allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(nil) + + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), + params: { merge_when_pipeline_succeeds: true } + + expect(response).to have_gitlab_http_status(405) + expect(json_response['message']).to eq('Not allowed: pipeline does not exist') + end + end + it "returns 404 for an invalid merge request IID" do put api("/projects/#{project.id}/merge_requests/12345/merge", user) diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 577f61ae8d0..16d306f39cd 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -504,8 +504,9 @@ describe API::Projects do project4.add_reporter(user2) end - it 'returns an array of groups the user has at least developer access' do + it 'returns an array of projects the user has at least developer access' do get api('/projects', user2), params: { min_access_level: 30 } + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index b331da1acba..4006e697a41 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -542,6 +542,30 @@ describe API::Runner, :clean_gitlab_redis_shared_state do end end + context 'when job filtered by job_age' do + let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, queued_at: 60.seconds.ago) } + + context 'job is queued less than job_age parameter' do + let(:job_age) { 120 } + + it 'gives 204' do + request_job(job_age: job_age) + + expect(response).to have_gitlab_http_status(204) + end + end + + context 'job is queued more than job_age parameter' do + let(:job_age) { 30 } + + it 'picks a job' do + request_job(job_age: job_age) + + expect(response).to have_gitlab_http_status(201) + end + end + end + context 'when job is made for branch' do it 'sets tag as ref_type' do request_job diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb index 49672591b3b..7d61ec9c4d8 100644 --- a/spec/requests/api/search_spec.rb +++ b/spec/requests/api/search_spec.rb @@ -414,6 +414,13 @@ describe API::Search do expect(response).to have_gitlab_http_status(200) expect(json_response.size).to eq(11) end + + it 'by ref' do + get api("/projects/#{repo_project.id}/search", user), params: { scope: 'blobs', search: 'This file is used in tests for ci_environments_status', ref: 'pages-deploy' } + + expect(response).to have_gitlab_http_status(200) + expect(json_response.size).to eq(1) + end end end end diff --git a/spec/serializers/test_case_entity_spec.rb b/spec/serializers/test_case_entity_spec.rb index a55910f98bb..986c9feb07b 100644 --- a/spec/serializers/test_case_entity_spec.rb +++ b/spec/serializers/test_case_entity_spec.rb @@ -14,6 +14,7 @@ describe TestCaseEntity do it 'contains correct test case details' do expect(subject[:status]).to eq('success') expect(subject[:name]).to eq('Test#sum when a is 1 and b is 3 returns summary') + expect(subject[:classname]).to eq('spec.test_spec') expect(subject[:execution_time]).to eq(1.11) end end @@ -24,6 +25,7 @@ describe TestCaseEntity do it 'contains correct test case details' do expect(subject[:status]).to eq('failed') expect(subject[:name]).to eq('Test#sum when a is 2 and b is 2 returns summary') + expect(subject[:classname]).to eq('spec.test_spec') expect(subject[:execution_time]).to eq(2.22) end end diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index e24fe60f059..4f4776bbb27 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -285,7 +285,7 @@ describe Auth::ContainerRegistryAuthenticationService do it_behaves_like 'not a container repository factory' end - context 'disallow guest to delete images since regsitry 2.7' do + context 'disallow guest to delete images since registry 2.7' do before do project.add_guest(current_user) end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 8a80652b3d8..9a3ac75e418 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -773,7 +773,7 @@ describe Ci::CreatePipelineService do end end - describe 'Merge request pipelines' do + describe 'Pipelines for merge requests' do let(:pipeline) do execute_service(source: source, merge_request: merge_request, @@ -817,12 +817,14 @@ describe Ci::CreatePipelineService do let(:merge_request) do create(:merge_request, source_project: project, - source_branch: Gitlab::Git.ref_name(ref_name), + source_branch: 'feature', target_project: project, target_branch: 'master') end - it 'creates a merge request pipeline' do + let(:ref_name) { merge_request.ref_path } + + it 'creates a detached merge request pipeline' do expect(pipeline).to be_persisted expect(pipeline).to be_merge_request_event expect(pipeline.merge_request).to eq(merge_request) @@ -837,6 +839,13 @@ describe Ci::CreatePipelineService do expect(pipeline.target_sha).to be_nil end + it 'schedules update for the head pipeline of the merge request' do + expect(UpdateHeadPipelineForMergeRequestWorker) + .to receive(:perform_async).with(merge_request.id) + + pipeline + end + context 'when target sha is specified' do let(:target_sha) { merge_request.target_branch_sha } @@ -858,15 +867,16 @@ describe Ci::CreatePipelineService do let(:merge_request) do create(:merge_request, source_project: project, - source_branch: Gitlab::Git.ref_name(ref_name), + source_branch: 'feature', target_project: target_project, target_branch: 'master') end + let(:ref_name) { 'refs/heads/feature' } let!(:project) { fork_project(target_project, nil, repository: true) } let!(:target_project) { create(:project, :repository) } - it 'creates a merge request pipeline in the forked project' do + it 'creates a legacy detached merge request pipeline in the forked project' do expect(pipeline).to be_persisted expect(project.ci_pipelines).to eq([pipeline]) expect(target_project.ci_pipelines).to be_empty @@ -884,7 +894,7 @@ describe Ci::CreatePipelineService do } end - it 'does not create a merge request pipeline' do + it 'does not create a detached merge request pipeline' do expect(pipeline).not_to be_persisted expect(pipeline.errors[:base]).to eq(["No stages / jobs for this pipeline."]) end @@ -894,7 +904,7 @@ describe Ci::CreatePipelineService do context 'when merge request is not specified' do let(:merge_request) { nil } - it 'does not create a merge request pipeline' do + it 'does not create a detached merge request pipeline' do expect(pipeline).not_to be_persisted expect(pipeline.errors[:merge_request]).to eq(["can't be blank"]) end @@ -928,7 +938,7 @@ describe Ci::CreatePipelineService do target_branch: 'master') end - it 'does not create a merge request pipeline' do + it 'does not create a detached merge request pipeline' do expect(pipeline).not_to be_persisted expect(pipeline.errors[:base]) @@ -939,7 +949,7 @@ describe Ci::CreatePipelineService do context 'when merge request is not specified' do let(:merge_request) { nil } - it 'does not create a merge request pipeline' do + it 'does not create a detached merge request pipeline' do expect(pipeline).not_to be_persisted expect(pipeline.errors[:base]) @@ -968,7 +978,7 @@ describe Ci::CreatePipelineService do target_branch: 'master') end - it 'does not create a merge request pipeline' do + it 'does not create a detached merge request pipeline' do expect(pipeline).not_to be_persisted expect(pipeline.errors[:base]) @@ -999,7 +1009,7 @@ describe Ci::CreatePipelineService do target_branch: 'master') end - it 'does not create a merge request pipeline' do + it 'does not create a detached merge request pipeline' do expect(pipeline).not_to be_persisted expect(pipeline.errors[:base]) @@ -1028,7 +1038,7 @@ describe Ci::CreatePipelineService do target_branch: 'master') end - it 'does not create a merge request pipeline' do + it 'does not create a detached merge request pipeline' do expect(pipeline).not_to be_persisted expect(pipeline.errors[:base]) diff --git a/spec/services/clusters/build_service_spec.rb b/spec/services/clusters/build_service_spec.rb index da0cb42b3a1..f3e852726f4 100644 --- a/spec/services/clusters/build_service_spec.rb +++ b/spec/services/clusters/build_service_spec.rb @@ -21,5 +21,13 @@ describe Clusters::BuildService do is_expected.to be_group_type end end + + describe 'when cluster subject is an instance' do + let(:cluster_subject) { Clusters::Instance.new } + + it 'sets the cluster_type to instance_type' do + is_expected.to be_instance_type + end + end end end diff --git a/spec/services/clusters/refresh_service_spec.rb b/spec/services/clusters/refresh_service_spec.rb index 94c35228955..5bc8a709941 100644 --- a/spec/services/clusters/refresh_service_spec.rb +++ b/spec/services/clusters/refresh_service_spec.rb @@ -93,32 +93,14 @@ describe Clusters::RefreshService do let(:group) { cluster.group } let(:project) { create(:project, group: group) } - context 'when ci_preparing_state feature flag is enabled' do - include_examples 'does not create a kubernetes namespace' - - context 'when project already has kubernetes namespace' do - before do - create(:cluster_kubernetes_namespace, project: project, cluster: cluster) - end - - include_examples 'does not create a kubernetes namespace' - end - end + include_examples 'does not create a kubernetes namespace' - context 'when ci_preparing_state feature flag is disabled' do + context 'when project already has kubernetes namespace' do before do - stub_feature_flags(ci_preparing_state: false) + create(:cluster_kubernetes_namespace, project: project, cluster: cluster) end - include_examples 'creates a kubernetes namespace' - - context 'when project already has kubernetes namespace' do - before do - create(:cluster_kubernetes_namespace, project: project, cluster: cluster) - end - - include_examples 'does not create a kubernetes namespace' - end + include_examples 'does not create a kubernetes namespace' end end diff --git a/spec/services/git/base_hooks_service_spec.rb b/spec/services/git/base_hooks_service_spec.rb new file mode 100644 index 00000000000..4a2ec769116 --- /dev/null +++ b/spec/services/git/base_hooks_service_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Git::BaseHooksService do + include RepoHelpers + include GitHelpers + + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:service) { described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) } + + let(:oldrev) { Gitlab::Git::BLANK_SHA } + let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0 + let(:ref) { 'refs/tags/v1.1.0' } + + describe 'with remote mirrors' do + class TestService < described_class + def commits + [] + end + end + + let(:project) { create(:project, :repository, :remote_mirror) } + + subject { TestService.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) } + + before do + expect(subject).to receive(:execute_project_hooks) + end + + context 'when remote mirror feature is enabled' do + it 'fails stuck remote mirrors' do + allow(project).to receive(:update_remote_mirrors).and_return(project.remote_mirrors) + expect(project).to receive(:mark_stuck_remote_mirrors_as_failed!) + + subject.execute + end + + it 'updates remote mirrors' do + expect(project).to receive(:update_remote_mirrors) + + subject.execute + end + end + + context 'when remote mirror feature is disabled' do + before do + stub_application_setting(mirror_available: false) + end + + context 'with remote mirrors global setting overridden' do + before do + project.remote_mirror_available_overridden = true + end + + it 'fails stuck remote mirrors' do + allow(project).to receive(:update_remote_mirrors).and_return(project.remote_mirrors) + expect(project).to receive(:mark_stuck_remote_mirrors_as_failed!) + + subject.execute + end + + it 'updates remote mirrors' do + expect(project).to receive(:update_remote_mirrors) + + subject.execute + end + end + + context 'without remote mirrors global setting overridden' do + before do + project.remote_mirror_available_overridden = false + end + + it 'does not fails stuck remote mirrors' do + expect(project).not_to receive(:mark_stuck_remote_mirrors_as_failed!) + + subject.execute + end + + it 'does not updates remote mirrors' do + expect(project).not_to receive(:update_remote_mirrors) + + subject.execute + end + end + end + end +end diff --git a/spec/services/git/branch_hooks_service_spec.rb b/spec/services/git/branch_hooks_service_spec.rb index 4895e762602..22faa996015 100644 --- a/spec/services/git/branch_hooks_service_spec.rb +++ b/spec/services/git/branch_hooks_service_spec.rb @@ -18,6 +18,12 @@ describe Git::BranchHooksService do described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) end + it 'update remote mirrors' do + expect(service).to receive(:update_remote_mirrors).and_call_original + + service.execute + end + describe "Git Push Data" do subject(:push_data) { service.execute } diff --git a/spec/services/git/branch_push_service_spec.rb b/spec/services/git/branch_push_service_spec.rb index ad21f710833..6e39fa6b3c0 100644 --- a/spec/services/git/branch_push_service_spec.rb +++ b/spec/services/git/branch_push_service_spec.rb @@ -17,72 +17,6 @@ describe Git::BranchPushService, services: true do project.add_maintainer(user) end - describe 'with remote mirrors' do - let(:project) { create(:project, :repository, :remote_mirror) } - - subject do - described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) - end - - context 'when remote mirror feature is enabled' do - it 'fails stuck remote mirrors' do - allow(project).to receive(:update_remote_mirrors).and_return(project.remote_mirrors) - expect(project).to receive(:mark_stuck_remote_mirrors_as_failed!) - - subject.execute - end - - it 'updates remote mirrors' do - expect(project).to receive(:update_remote_mirrors) - - subject.execute - end - end - - context 'when remote mirror feature is disabled' do - before do - stub_application_setting(mirror_available: false) - end - - context 'with remote mirrors global setting overridden' do - before do - project.remote_mirror_available_overridden = true - end - - it 'fails stuck remote mirrors' do - allow(project).to receive(:update_remote_mirrors).and_return(project.remote_mirrors) - expect(project).to receive(:mark_stuck_remote_mirrors_as_failed!) - - subject.execute - end - - it 'updates remote mirrors' do - expect(project).to receive(:update_remote_mirrors) - - subject.execute - end - end - - context 'without remote mirrors global setting overridden' do - before do - project.remote_mirror_available_overridden = false - end - - it 'does not fails stuck remote mirrors' do - expect(project).not_to receive(:mark_stuck_remote_mirrors_as_failed!) - - subject.execute - end - - it 'does not updates remote mirrors' do - expect(project).not_to receive(:update_remote_mirrors) - - subject.execute - end - end - end - end - describe 'Push branches' do subject do execute_service(project, user, oldrev, newrev, ref) diff --git a/spec/services/git/tag_hooks_service_spec.rb b/spec/services/git/tag_hooks_service_spec.rb index f4c02932f98..f5938a5c708 100644 --- a/spec/services/git/tag_hooks_service_spec.rb +++ b/spec/services/git/tag_hooks_service_spec.rb @@ -18,6 +18,12 @@ describe Git::TagHooksService, :service do described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) end + it 'update remote mirrors' do + expect(service).to receive(:update_remote_mirrors).and_call_original + + service.execute + end + describe 'System hooks' do it 'Executes system hooks' do push_data = service.execute diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb index fce9eed8b08..6874a8a0929 100644 --- a/spec/services/issues/close_service_spec.rb +++ b/spec/services/issues/close_service_spec.rb @@ -3,11 +3,13 @@ require 'spec_helper' describe Issues::CloseService do - let(:user) { create(:user) } - let(:user2) { create(:user) } + let(:project) { create(:project, :repository) } + let(:user) { create(:user, email: "user@example.com") } + let(:user2) { create(:user, email: "user2@example.com") } let(:guest) { create(:user) } - let(:issue) { create(:issue, assignees: [user2], author: create(:user)) } - let(:project) { issue.project } + let(:issue) { create(:issue, title: "My issue", project: project, assignees: [user2], author: create(:user)) } + let(:closing_merge_request) { create(:merge_request, source_project: project) } + let(:closing_commit) { create(:commit, project: project) } let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) } before do @@ -39,7 +41,7 @@ describe Issues::CloseService do .and_return(true) expect(service).to receive(:close_issue) - .with(issue, commit: nil, notifications: true, system_note: true) + .with(issue, closed_via: nil, notifications: true, system_note: true) service.execute(issue) end @@ -57,6 +59,38 @@ describe Issues::CloseService do end describe '#close_issue' do + context "closed by a merge request" do + before do + perform_enqueued_jobs do + described_class.new(project, user).close_issue(issue, closed_via: closing_merge_request) + end + end + + it 'mentions closure via a merge request' do + email = ActionMailer::Base.deliveries.last + + expect(email.to.first).to eq(user2.email) + expect(email.subject).to include(issue.title) + expect(email.body.parts.map(&:body)).to all(include(closing_merge_request.to_reference)) + end + end + + context "closed by a commit" do + before do + perform_enqueued_jobs do + described_class.new(project, user).close_issue(issue, closed_via: closing_commit) + end + end + + it 'mentions closure via a commit' do + email = ActionMailer::Base.deliveries.last + + expect(email.to.first).to eq(user2.email) + expect(email.subject).to include(issue.title) + expect(email.body.parts.map(&:body)).to all(include(closing_commit.id)) + end + end + context "valid params" do before do perform_enqueued_jobs do diff --git a/spec/services/members/create_service_spec.rb b/spec/services/members/create_service_spec.rb index 2c4fb131ed9..674fe0f666e 100644 --- a/spec/services/members/create_service_spec.rb +++ b/spec/services/members/create_service_spec.rb @@ -44,7 +44,18 @@ describe Members::CreateService do result = described_class.new(user, params).execute(project) expect(result[:status]).to eq(:error) - expect(result[:message]).to include(project_user.username) + expect(result[:message]).to include("#{project_user.username}: Access level is not included in the list") expect(project.users).not_to include project_user end + + it 'does not add a member with an existing invite' do + invited_member = create(:project_member, :invited, project: project) + + params = { user_ids: invited_member.invite_email, + access_level: Gitlab::Access::GUEST } + result = described_class.new(user, params).execute(project) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq('Invite email has already been taken') + end end diff --git a/spec/services/merge_requests/rebase_service_spec.rb b/spec/services/merge_requests/rebase_service_spec.rb index a443e4588d9..7e2f03d1097 100644 --- a/spec/services/merge_requests/rebase_service_spec.rb +++ b/spec/services/merge_requests/rebase_service_spec.rb @@ -38,6 +38,32 @@ describe MergeRequests::RebaseService do end end + shared_examples 'sequence of failure and success' do + it 'properly clears the error message' do + allow(repository).to receive(:gitaly_operation_client).and_raise('Something went wrong') + + service.execute(merge_request) + + expect(merge_request.reload.merge_error).to eq described_class::REBASE_ERROR + + allow(repository).to receive(:gitaly_operation_client).and_call_original + + service.execute(merge_request) + + expect(merge_request.reload.merge_error).to eq nil + end + end + + it_behaves_like 'sequence of failure and success' + + context 'with deprecated step rebase feature' do + before do + stub_feature_flags(two_step_rebase: false) + end + + it_behaves_like 'sequence of failure and success' + end + context 'when unexpected error occurs' do before do allow(repository).to receive(:gitaly_operation_client).and_raise('Something went wrong') diff --git a/spec/services/projects/after_import_service_spec.rb b/spec/services/projects/after_import_service_spec.rb index 95c11f71c5e..51d3fd18881 100644 --- a/spec/services/projects/after_import_service_spec.rb +++ b/spec/services/projects/after_import_service_spec.rb @@ -15,7 +15,7 @@ describe Projects::AfterImportService do describe '#execute' do before do allow(Projects::HousekeepingService) - .to receive(:new).with(project, :gc).and_return(housekeeping_service) + .to receive(:new).with(project).and_return(housekeeping_service) allow(housekeeping_service) .to receive(:execute).and_yield diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index bd7a0c68766..f54f9200661 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -268,33 +268,6 @@ describe Projects::CreateService, '#execute' do end end - context 'when group has kubernetes cluster' do - let(:group_cluster) { create(:cluster, :group, :provided_by_gcp) } - let(:group) { group_cluster.group } - - let(:token) { 'aaaa' } - let(:service_account_creator) { double(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService, execute: true) } - let(:secrets_fetcher) { double(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService, execute: token) } - - before do - group.add_owner(user) - - stub_feature_flags(ci_preparing_state: false) - expect(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService).to receive(:namespace_creator).and_return(service_account_creator) - expect(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService).to receive(:new).and_return(secrets_fetcher) - end - - it 'creates kubernetes namespace for the project' do - project = create_project(user, opts.merge!(namespace_id: group.id)) - - expect(project).to be_valid - - kubernetes_namespace = group_cluster.kubernetes_namespaces.first - expect(kubernetes_namespace).to be_present - expect(kubernetes_namespace.project).to eq(project) - end - end - context 'when there is an active service template' do before do create(:service, project: nil, template: true, active: true) diff --git a/spec/services/projects/detect_repository_languages_service_spec.rb b/spec/services/projects/detect_repository_languages_service_spec.rb index e3e561c971c..df5eed18ac0 100644 --- a/spec/services/projects/detect_repository_languages_service_spec.rb +++ b/spec/services/projects/detect_repository_languages_service_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' describe Projects::DetectRepositoryLanguagesService, :clean_gitlab_redis_shared_state do set(:project) { create(:project, :repository) } - subject { described_class.new(project, project.owner) } + subject { described_class.new(project) } describe '#execute' do context 'without previous detection' do diff --git a/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb index d8427d0bf78..80debcd3a7a 100644 --- a/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb +++ b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb @@ -33,7 +33,10 @@ describe Projects::LfsPointers::LfsDownloadLinkListService do before do allow(project).to receive(:lfs_enabled?).and_return(true) - allow(Gitlab::HTTP).to receive(:post).and_return(objects_response) + response = instance_double(HTTParty::Response) + allow(response).to receive(:body).and_return(objects_response.to_json) + allow(response).to receive(:success?).and_return(true) + allow(Gitlab::HTTP).to receive(:post).and_return(response) end describe '#execute' do @@ -89,6 +92,21 @@ describe Projects::LfsPointers::LfsDownloadLinkListService do expect { subject.send(:get_download_links, new_oids) }.to raise_error(described_class::DownloadLinksError) end + + shared_examples 'JSON parse errors' do |body| + it 'raises error' do + response = instance_double(HTTParty::Response) + allow(response).to receive(:body).and_return(body) + allow(response).to receive(:success?).and_return(true) + allow(Gitlab::HTTP).to receive(:post).and_return(response) + + expect { subject.send(:get_download_links, new_oids) }.to raise_error(described_class::DownloadLinksError) + end + end + + it_behaves_like 'JSON parse errors', '{' + it_behaves_like 'JSON parse errors', '{}' + it_behaves_like 'JSON parse errors', '{ foo: 123 }' end describe '#parse_response_links' do diff --git a/spec/services/projects/repository_languages_service_spec.rb b/spec/services/projects/repository_languages_service_spec.rb index 09c61363ad2..46c5095327d 100644 --- a/spec/services/projects/repository_languages_service_spec.rb +++ b/spec/services/projects/repository_languages_service_spec.rb @@ -10,7 +10,7 @@ describe Projects::RepositoryLanguagesService do context 'when a project is without detected programming languages' do it 'schedules a worker and returns the empty result' do - expect(::DetectRepositoryLanguagesWorker).to receive(:perform_async).with(project.id, project.owner.id) + expect(::DetectRepositoryLanguagesWorker).to receive(:perform_async).with(project.id) expect(service.execute).to eq([]) end end @@ -19,7 +19,7 @@ describe Projects::RepositoryLanguagesService do let!(:repository_language) { create(:repository_language, project: project) } it 'does not schedule a worker and returns the detected languages' do - expect(::DetectRepositoryLanguagesWorker).not_to receive(:perform_async).with(project.id, project.owner.id) + expect(::DetectRepositoryLanguagesWorker).not_to receive(:perform_async).with(project.id) languages = service.execute diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 9acc3657fa9..a47c10d991a 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -73,33 +73,6 @@ describe Projects::TransferService do shard_name: project.repository_storage ) end - - context 'new group has a kubernetes cluster' do - let(:group_cluster) { create(:cluster, :group, :provided_by_gcp) } - let(:group) { group_cluster.group } - - let(:token) { 'aaaa' } - let(:service_account_creator) { double(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService, execute: true) } - let(:secrets_fetcher) { double(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService, execute: token) } - - subject { transfer_project(project, user, group) } - - before do - stub_feature_flags(ci_preparing_state: false) - expect(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService).to receive(:namespace_creator).and_return(service_account_creator) - expect(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService).to receive(:new).and_return(secrets_fetcher) - end - - it 'creates kubernetes namespace for the project' do - subject - - expect(project.kubernetes_namespaces.count).to eq(1) - - kubernetes_namespace = group_cluster.kubernetes_namespaces.first - expect(kubernetes_namespace).to be_present - expect(kubernetes_namespace.project).to eq(project) - end - end end context 'when transfer fails' do diff --git a/spec/services/users/update_service_spec.rb b/spec/services/users/update_service_spec.rb index e04c05418b0..9384287f98a 100644 --- a/spec/services/users/update_service_spec.rb +++ b/spec/services/users/update_service_spec.rb @@ -13,6 +13,15 @@ describe Users::UpdateService do expect(user.name).to eq('New Name') end + it 'updates time preferences' do + result = update_user(user, timezone: 'Europe/Warsaw', time_display_relative: true, time_format_in_24h: false) + + expect(result).to eq(status: :success) + expect(user.reload.timezone).to eq('Europe/Warsaw') + expect(user.time_display_relative).to eq(true) + expect(user.time_format_in_24h).to eq(false) + end + it 'returns an error result when record cannot be updated' do result = {} expect do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9266bee34d6..69589c9aa33 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -138,7 +138,7 @@ RSpec.configure do |config| .and_return(false) end - config.before(:example, :quarantine) do + config.around(:example, :quarantine) do # Skip tests in quarantine unless we explicitly focus on them. skip('In quarantine') unless config.inclusion_filter[:quarantine] end diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 18a7a392c12..875a9a76e12 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -17,6 +17,8 @@ JS_CONSOLE_FILTER = Regexp.union([ "Download the Vue Devtools extension" ]) +CAPYBARA_WINDOW_SIZE = [1366, 768].freeze + Capybara.register_driver :chrome do |app| capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( # This enables access to logs with `page.driver.manage.get_log(:browser)` @@ -29,7 +31,7 @@ Capybara.register_driver :chrome do |app| ) options = Selenium::WebDriver::Chrome::Options.new - options.add_argument("window-size=1240,1400") + options.add_argument("window-size=#{CAPYBARA_WINDOW_SIZE.join(',')}") # Chrome won't work properly in a Docker container in sandbox mode options.add_argument("no-sandbox") @@ -78,8 +80,11 @@ RSpec.configure do |config| protocol: 'http') # reset window size between tests - unless session.current_window.size == [1240, 1400] - session.current_window.resize_to(1240, 1400) rescue nil + unless session.current_window.size == CAPYBARA_WINDOW_SIZE + begin + session.current_window.resize_to(*CAPYBARA_WINDOW_SIZE) + rescue # ? + end end end diff --git a/spec/support/features/variable_list_shared_examples.rb b/spec/support/features/variable_list_shared_examples.rb index 693b796fbdc..92a19dd22a2 100644 --- a/spec/support/features/variable_list_shared_examples.rb +++ b/spec/support/features/variable_list_shared_examples.rb @@ -17,7 +17,7 @@ shared_examples 'variable list' do visit page_path # We check the first row because it re-sorts to alphabetical order on refresh - page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do + page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do expect(find('.js-ci-variable-input-key').value).to eq('key') expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key_value') end @@ -38,7 +38,7 @@ shared_examples 'variable list' do visit page_path # We check the first row because it re-sorts to alphabetical order on refresh - page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do + page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do expect(find('.js-ci-variable-input-key').value).to eq('key') expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key_value') expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true') @@ -59,7 +59,7 @@ shared_examples 'variable list' do visit page_path # We check the first row because it re-sorts to alphabetical order on refresh - page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do + page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do expect(find('.js-ci-variable-input-key').value).to eq('key') expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key_value') expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true') @@ -116,19 +116,19 @@ shared_examples 'variable list' do page.within('.js-ci-variable-list-section') do expect(first('.js-ci-variable-input-key').value).to eq(variable.key) expect(first('.js-ci-variable-input-value', visible: false).value).to eq(variable.value) - expect(page).to have_content('*' * 20) + expect(page).to have_content('*' * 17) click_button('Reveal value') expect(first('.js-ci-variable-input-key').value).to eq(variable.key) expect(first('.js-ci-variable-input-value').value).to eq(variable.value) - expect(page).not_to have_content('*' * 20) + expect(page).not_to have_content('*' * 17) click_button('Hide value') expect(first('.js-ci-variable-input-key').value).to eq(variable.key) expect(first('.js-ci-variable-input-value', visible: false).value).to eq(variable.value) - expect(page).to have_content('*' * 20) + expect(page).to have_content('*' * 17) end end @@ -149,7 +149,7 @@ shared_examples 'variable list' do page.within('.js-ci-variable-list-section') do click_button('Reveal value') - page.within('.js-row:nth-child(1)') do + page.within('.js-row:nth-child(2)') do find('.js-ci-variable-input-key').set('new_key') find('.js-ci-variable-input-value').set('new_value') end @@ -159,7 +159,7 @@ shared_examples 'variable list' do visit page_path - page.within('.js-row:nth-child(1)') do + page.within('.js-row:nth-child(2)') do expect(find('.js-ci-variable-input-key').value).to eq('new_key') expect(find('.js-ci-variable-input-value', visible: false).value).to eq('new_value') end @@ -181,7 +181,7 @@ shared_examples 'variable list' do visit page_path # We check the first row because it re-sorts to alphabetical order on refresh - page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do + page.within('.js-ci-variable-list-section .js-row:nth-child(3)') do find('.ci-variable-protected-item .js-project-feature-toggle').click expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true') @@ -193,7 +193,7 @@ shared_examples 'variable list' do visit page_path # We check the first row because it re-sorts to alphabetical order on refresh - page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do + page.within('.js-ci-variable-list-section .js-row:nth-child(3)') do expect(find('.js-ci-variable-input-key').value).to eq('unprotected_key') expect(find('.js-ci-variable-input-value', visible: false).value).to eq('unprotected_value') expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true') @@ -215,7 +215,7 @@ shared_examples 'variable list' do visit page_path - page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do + page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do find('.ci-variable-protected-item .js-project-feature-toggle').click expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('false') @@ -226,7 +226,7 @@ shared_examples 'variable list' do visit page_path - page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do + page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do expect(find('.js-ci-variable-input-key').value).to eq('protected_key') expect(find('.js-ci-variable-input-value', visible: false).value).to eq('protected_value') expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('false') @@ -234,7 +234,7 @@ shared_examples 'variable list' do end it 'edits variable to be unmasked' do - page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do + page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true') find('.ci-variable-masked-item .js-project-feature-toggle').click @@ -247,13 +247,13 @@ shared_examples 'variable list' do visit page_path - page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do + page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false') end end it 'edits variable to be masked' do - page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do + page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true') find('.ci-variable-masked-item .js-project-feature-toggle').click @@ -266,7 +266,7 @@ shared_examples 'variable list' do visit page_path - page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do + page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false') find('.ci-variable-masked-item .js-project-feature-toggle').click @@ -279,7 +279,7 @@ shared_examples 'variable list' do visit page_path - page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do + page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true') end end @@ -302,7 +302,7 @@ shared_examples 'variable list' do expect(page).to have_selector('.js-row', count: 4) # Remove the `akey` variable - page.within('.js-row:nth-child(2)') do + page.within('.js-row:nth-child(3)') do first('.js-row-remove-button').click end diff --git a/spec/support/helpers/features/notes_helpers.rb b/spec/support/helpers/features/notes_helpers.rb index 89517fde6e2..8a139fafac2 100644 --- a/spec/support/helpers/features/notes_helpers.rb +++ b/spec/support/helpers/features/notes_helpers.rb @@ -23,8 +23,16 @@ module Spec def preview_note(text) page.within('.js-main-target-form') do - fill_in('note[note]', with: text) + filled_text = fill_in('note[note]', with: text) + + # Wait for quick action prompt to load and then dismiss it with ESC + # because it may block the Preview button + wait_for_requests + filled_text.send_keys(:escape) + click_on('Preview') + + yield if block_given? end end end diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb index b49d743fb9a..44ed9da25fc 100644 --- a/spec/support/helpers/graphql_helpers.rb +++ b/spec/support/helpers/graphql_helpers.rb @@ -102,6 +102,7 @@ module GraphqlHelpers def all_graphql_fields_for(class_name, parent_types = Set.new) allow_unlimited_graphql_complexity + allow_unlimited_graphql_depth type = GitlabSchema.types[class_name.to_s] return "" unless type @@ -190,4 +191,13 @@ module GraphqlHelpers allow_any_instance_of(GitlabSchema).to receive(:max_complexity).and_return nil allow(GitlabSchema).to receive(:max_query_complexity).with(any_args).and_return nil end + + def allow_unlimited_graphql_depth + allow_any_instance_of(GitlabSchema).to receive(:max_depth).and_return nil + allow(GitlabSchema).to receive(:max_query_depth).with(any_args).and_return nil + end end + +# This warms our schema, doing this as part of loading the helpers to avoid +# duplicate loading error when Rails tries autoload the types. +GitlabSchema.graphql_definition diff --git a/spec/support/helpers/javascript_fixtures_helpers.rb b/spec/support/helpers/javascript_fixtures_helpers.rb index 9cae8f934db..cdd7724cc13 100644 --- a/spec/support/helpers/javascript_fixtures_helpers.rb +++ b/spec/support/helpers/javascript_fixtures_helpers.rb @@ -11,11 +11,15 @@ module JavaScriptFixturesHelpers base.around do |example| # pick an arbitrary date from the past, so tests are not time dependent Timecop.freeze(Time.utc(2015, 7, 3, 10)) { example.run } + + raise NoMethodError.new('You need to set `response` for the fixture generator! This will automatically happen with `type: :controller` or `type: :request`.', 'response') unless respond_to?(:response) + + store_frontend_fixture(response, example.description) end end def fixture_root_path - 'spec/javascripts/fixtures' + (Gitlab.ee? ? 'ee/' : '') + 'spec/javascripts/fixtures' end # Public: Removes all fixture files from given directory @@ -29,7 +33,13 @@ module JavaScriptFixturesHelpers end end - # Public: Store a response object as fixture file + def remove_repository(project) + Gitlab::Shell.new.remove_repository(project.repository_storage, project.disk_path) + end + + private + + # Private: Store a response object as fixture file # # response - string or response object to store # fixture_file_name - file name to store the fixture in (relative to .fixture_root_path) @@ -42,12 +52,6 @@ module JavaScriptFixturesHelpers File.write(full_fixture_path, fixture) end - def remove_repository(project) - Gitlab::Shell.new.remove_repository(project.repository_storage, project.disk_path) - end - - private - # Private: Prepare a response object for use as a frontend fixture # # response - response object to prepare diff --git a/spec/support/helpers/lets_encrypt_helpers.rb b/spec/support/helpers/lets_encrypt_helpers.rb new file mode 100644 index 00000000000..7f0886b451c --- /dev/null +++ b/spec/support/helpers/lets_encrypt_helpers.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module LetsEncryptHelpers + def stub_lets_encrypt_client + client = instance_double('Acme::Client') + + allow(client).to receive(:new_account) + allow(client).to receive(:terms_of_service).and_return( + "https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf" + ) + + allow(Acme::Client).to receive(:new).with( + private_key: kind_of(OpenSSL::PKey::RSA), + directory: ::Gitlab::LetsEncrypt::Client::STAGING_DIRECTORY_URL + ).and_return(client) + + client + end +end diff --git a/spec/support/helpers/mobile_helpers.rb b/spec/support/helpers/mobile_helpers.rb index 9dc1f1de436..4230d315d9b 100644 --- a/spec/support/helpers/mobile_helpers.rb +++ b/spec/support/helpers/mobile_helpers.rb @@ -8,7 +8,7 @@ module MobileHelpers end def restore_window_size - resize_window(1366, 768) + resize_window(*CAPYBARA_WINDOW_SIZE) end def resize_window(width, height) diff --git a/spec/support/helpers/project_forks_helper.rb b/spec/support/helpers/project_forks_helper.rb index 9a86560da2a..bcb11a09b36 100644 --- a/spec/support/helpers/project_forks_helper.rb +++ b/spec/support/helpers/project_forks_helper.rb @@ -1,5 +1,11 @@ module ProjectForksHelper def fork_project(project, user = nil, params = {}) + Gitlab::GitalyClient.allow_n_plus_1_calls do + fork_project_direct(project, user, params) + end + end + + def fork_project_direct(project, user = nil, params = {}) # Load the `fork_network` for the project to fork as there might be one that # wasn't loaded yet. project.reload unless project.fork_network @@ -44,11 +50,16 @@ module ProjectForksHelper end def fork_project_with_submodules(project, user = nil, params = {}) - forked_project = fork_project(project, user, params) - TestEnv.copy_repo(forked_project, - bare_repo: TestEnv.forked_repo_path_bare, - refs: TestEnv::FORKED_BRANCH_SHA) - forked_project.repository.after_import - forked_project + Gitlab::GitalyClient.allow_n_plus_1_calls do + forked_project = fork_project_direct(project, user, params) + TestEnv.copy_repo( + forked_project, + bare_repo: TestEnv.forked_repo_path_bare, + refs: TestEnv::FORKED_BRANCH_SHA + ) + forked_project.repository.after_import + + forked_project + end end end diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index dc902d373b8..06b5ecdf150 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -135,7 +135,7 @@ module TestEnv def clean_gitlab_test_path Dir[TMP_TEST_PATH].each do |entry| - if File.basename(entry) =~ /\A(gitlab-(test|test_bare|test-fork|test-fork_bare))\z/ + unless test_dirs.include?(File.basename(entry)) FileUtils.rm_rf(entry) end end @@ -312,6 +312,18 @@ module TestEnv private + # These are directories that should be preserved at cleanup time + def test_dirs + @test_dirs ||= %w[ + gitaly + gitlab-shell + gitlab-test + gitlab-test_bare + gitlab-test-fork + gitlab-test-fork_bare + ] + end + def factory_repo_path @factory_repo_path ||= Rails.root.join('tmp', 'tests', factory_repo_name) end diff --git a/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb index e0d0b790a0e..a79a61bc708 100644 --- a/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb +++ b/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true shared_examples 'close quick action' do |issuable_type| + include Spec::Support::Helpers::Features::NotesHelpers + before do project.add_maintainer(maintainer) gitlab_sign_in(maintainer) @@ -76,10 +78,7 @@ shared_examples 'close quick action' do |issuable_type| it 'explains close quick action' do visit public_send("project_#{issuable_type}_path", project, issuable) - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "this is done, close\n/close" - click_on 'Preview' - + preview_note("this is done, close\n/close") do expect(page).not_to have_content '/close' expect(page).to have_content 'this is done, close' expect(page).to have_content "Closes this #{issuable_type.to_s.humanize.downcase}." diff --git a/spec/support/shared_examples/requests/api/discussions.rb b/spec/support/shared_examples/requests/api/discussions.rb index eff8e401bad..c3132c41f5b 100644 --- a/spec/support/shared_examples/requests/api/discussions.rb +++ b/spec/support/shared_examples/requests/api/discussions.rb @@ -1,4 +1,4 @@ -shared_examples 'discussions API' do |parent_type, noteable_type, id_name| +shared_examples 'discussions API' do |parent_type, noteable_type, id_name, can_reply_to_individual_notes: false| describe "GET /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions" do it "returns an array of discussions" do get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions", user) @@ -136,13 +136,25 @@ shared_examples 'discussions API' do |parent_type, noteable_type, id_name| expect(response).to have_gitlab_http_status(400) end - it "returns a 400 bad request error if discussion is individual note" do - note.update_attribute(:type, nil) + context 'when the discussion is an individual note' do + before do + note.update!(type: nil) - post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\ - "discussions/#{note.discussion_id}/notes", user), params: { body: 'hi!' } + post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\ + "discussions/#{note.discussion_id}/notes", user), params: { body: 'hi!' } + end - expect(response).to have_gitlab_http_status(400) + if can_reply_to_individual_notes + it 'creates a new discussion' do + expect(response).to have_gitlab_http_status(201) + expect(json_response['body']).to eq('hi!') + expect(json_response['type']).to eq('DiscussionNote') + end + else + it 'returns 400 bad request' do + expect(response).to have_gitlab_http_status(400) + end + end end end diff --git a/spec/support/shared_examples/requests/api/issues_shared_example_spec.rb b/spec/support/shared_examples/requests/api/issues_shared_example_spec.rb new file mode 100644 index 00000000000..1133e95e44e --- /dev/null +++ b/spec/support/shared_examples/requests/api/issues_shared_example_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +shared_examples 'labeled issues with labels and label_name params' do + shared_examples 'returns label names' do + it 'returns label names' do + expect_paginated_array_response(issue.id) + expect(json_response.first['labels']).to eq([label_c.title, label_b.title, label.title]) + end + end + + shared_examples 'returns basic label entity' do + it 'returns basic label entity' do + expect_paginated_array_response(issue.id) + expect(json_response.first['labels'].pluck('name')).to eq([label_c.title, label_b.title, label.title]) + expect(json_response.first['labels'].first).to match_schema('/public_api/v4/label_basic') + end + end + + context 'array of labeled issues when all labels match' do + let(:params) { { labels: "#{label.title},#{label_b.title},#{label_c.title}" } } + + it_behaves_like 'returns label names' + end + + context 'array of labeled issues when all labels match with labels param as array' do + let(:params) { { labels: [label.title, label_b.title, label_c.title] } } + + it_behaves_like 'returns label names' + end + + context 'when with_labels_details provided' do + context 'array of labeled issues when all labels match' do + let(:params) { { labels: "#{label.title},#{label_b.title},#{label_c.title}", with_labels_details: true } } + + it_behaves_like 'returns basic label entity' + end + + context 'array of labeled issues when all labels match with labels param as array' do + let(:params) { { labels: [label.title, label_b.title, label_c.title], with_labels_details: true } } + + it_behaves_like 'returns basic label entity' + end + end +end diff --git a/spec/uploaders/import_export_uploader_spec.rb b/spec/uploaders/import_export_uploader_spec.rb index 825c1cabc14..2dea48e3a88 100644 --- a/spec/uploaders/import_export_uploader_spec.rb +++ b/spec/uploaders/import_export_uploader_spec.rb @@ -3,9 +3,18 @@ require 'spec_helper' describe ImportExportUploader do let(:model) { build_stubbed(:import_export_upload) } let(:upload) { create(:upload, model: model) } + let(:import_export_upload) { ImportExportUpload.new } subject { described_class.new(model, :import_file) } + context 'local store' do + describe '#move_to_store' do + it 'returns true' do + expect(subject.move_to_store).to be true + end + end + end + context "object_store is REMOTE" do before do stub_uploads_object_storage @@ -16,5 +25,28 @@ describe ImportExportUploader do it_behaves_like 'builds correct paths', store_dir: %r[import_export_upload/import_file/], upload_path: %r[import_export_upload/import_file/] + + describe '#move_to_store' do + it 'returns false' do + expect(subject.move_to_store).to be false + end + end + + describe 'with an export file directly uploaded' do + let(:tempfile) { Tempfile.new(['test', '.gz']) } + + before do + stub_uploads_object_storage(described_class, direct_upload: true) + import_export_upload.export_file = tempfile + end + + it 'cleans up cached file' do + cache_dir = File.join(import_export_upload.export_file.cache_path(nil), '*') + + import_export_upload.save! + + expect(Dir[cache_dir]).to be_empty + end + end end end diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb index a62830c35f1..6bad5d49b1c 100644 --- a/spec/uploaders/object_storage_spec.rb +++ b/spec/uploaders/object_storage_spec.rb @@ -12,7 +12,7 @@ class Implementation < GitlabUploader # user/:id def dynamic_segment - File.join(model.class.to_s.underscore, model.id.to_s) + File.join(model.class.underscore, model.id.to_s) end end diff --git a/spec/uploaders/personal_file_uploader_spec.rb b/spec/uploaders/personal_file_uploader_spec.rb index 97758f0243e..d9f0e2f3cb7 100644 --- a/spec/uploaders/personal_file_uploader_spec.rb +++ b/spec/uploaders/personal_file_uploader_spec.rb @@ -7,33 +7,19 @@ describe PersonalFileUploader do subject { uploader } - it_behaves_like 'builds correct paths', - store_dir: %r[uploads/-/system/personal_snippet/\d+], - upload_path: %r[\h+/\S+], - absolute_path: %r[#{CarrierWave.root}/uploads/-/system/personal_snippet\/\d+\/\h+\/\S+$] - - context "object_store is REMOTE" do + shared_examples '#base_dir' do before do - stub_uploads_object_storage + subject.instance_variable_set(:@secret, 'secret') end - include_context 'with storage', described_class::Store::REMOTE - - it_behaves_like 'builds correct paths', - store_dir: %r[\d+/\h+], - upload_path: %r[^personal_snippet\/\d+\/\h+\/<filename>] - end - - describe '#upload_paths' do - it 'builds correct paths for both local and remote storage' do - paths = uploader.upload_paths('test.jpg') + it 'is prefixed with uploads/-/system' do + allow(uploader).to receive(:file).and_return(double(extension: 'txt', filename: 'file_name')) - expect(paths.first).to match(%r[\h+\/test.jpg]) - expect(paths.second).to match(%r[^personal_snippet\/\d+\/\h+\/test.jpg]) + expect(described_class.base_dir(model)).to eq("uploads/-/system/personal_snippet/#{model.id}") end end - describe '#to_h' do + shared_examples '#to_h' do before do subject.instance_variable_set(:@secret, 'secret') end @@ -50,6 +36,40 @@ describe PersonalFileUploader do end end + describe '#upload_paths' do + it 'builds correct paths for both local and remote storage' do + paths = uploader.upload_paths('test.jpg') + + expect(paths.first).to match(%r[\h+\/test.jpg]) + expect(paths.second).to match(%r[^personal_snippet\/\d+\/\h+\/test.jpg]) + end + end + + context 'object_store is LOCAL' do + it_behaves_like 'builds correct paths', + store_dir: %r[uploads/-/system/personal_snippet/\d+/\h+], + upload_path: %r[\h+/\S+], + absolute_path: %r[#{CarrierWave.root}/uploads/-/system/personal_snippet\/\d+\/\h+\/\S+$] + + it_behaves_like '#base_dir' + it_behaves_like '#to_h' + end + + context "object_store is REMOTE" do + before do + stub_uploads_object_storage + end + + include_context 'with storage', described_class::Store::REMOTE + + it_behaves_like 'builds correct paths', + store_dir: %r[\d+/\h+], + upload_path: %r[^personal_snippet\/\d+\/\h+\/<filename>] + + it_behaves_like '#base_dir' + it_behaves_like '#to_h' + end + describe "#migrate!" do before do uploader.store!(fixture_file_upload('spec/fixtures/doc_sample.txt')) diff --git a/spec/views/projects/tree/show.html.haml_spec.rb b/spec/views/projects/tree/show.html.haml_spec.rb index 3b098320ad7..5bb0173ab89 100644 --- a/spec/views/projects/tree/show.html.haml_spec.rb +++ b/spec/views/projects/tree/show.html.haml_spec.rb @@ -7,6 +7,8 @@ describe 'projects/tree/show' do let(:repository) { project.repository } before do + stub_feature_flags(vue_file_list: false) + assign(:project, project) assign(:repository, repository) assign(:lfs_blob_ids, []) diff --git a/spec/workers/cluster_configure_worker_spec.rb b/spec/workers/cluster_configure_worker_spec.rb index daf014ac574..975088f3ee6 100644 --- a/spec/workers/cluster_configure_worker_spec.rb +++ b/spec/workers/cluster_configure_worker_spec.rb @@ -4,11 +4,6 @@ require 'spec_helper' describe ClusterConfigureWorker, '#perform' do let(:worker) { described_class.new } - let(:ci_preparing_state_enabled) { false } - - before do - stub_feature_flags(ci_preparing_state: ci_preparing_state_enabled) - end shared_examples 'configured cluster' do it 'creates a namespace' do @@ -33,26 +28,14 @@ describe ClusterConfigureWorker, '#perform' do context 'when group has a project' do let!(:project) { create(:project, group: group) } - it_behaves_like 'configured cluster' - - context 'ci_preparing_state feature is enabled' do - let(:ci_preparing_state_enabled) { true } - - it_behaves_like 'unconfigured cluster' - end + it_behaves_like 'unconfigured cluster' end context 'when group has project in a sub-group' do let!(:subgroup) { create(:group, parent: group) } let!(:project) { create(:project, group: subgroup) } - it_behaves_like 'configured cluster' - - context 'ci_preparing_state feature is enabled' do - let(:ci_preparing_state_enabled) { true } - - it_behaves_like 'unconfigured cluster' - end + it_behaves_like 'unconfigured cluster' end end diff --git a/spec/workers/detect_repository_languages_worker_spec.rb b/spec/workers/detect_repository_languages_worker_spec.rb index dbf32555985..755eb8dbf6b 100644 --- a/spec/workers/detect_repository_languages_worker_spec.rb +++ b/spec/workers/detect_repository_languages_worker_spec.rb @@ -4,7 +4,6 @@ require 'spec_helper' describe DetectRepositoryLanguagesWorker do set(:project) { create(:project) } - let(:user) { project.owner } subject { described_class.new } @@ -14,19 +13,13 @@ describe DetectRepositoryLanguagesWorker do allow(::Projects::DetectRepositoryLanguagesService).to receive(:new).and_return(service) expect(service).to receive(:execute) - subject.perform(project.id, user.id) + subject.perform(project.id) end context 'when invalid ids are used' do it 'does not raise when the project could not be found' do expect do - subject.perform(-1, user.id) - end.not_to raise_error - end - - it 'does not raise when the user could not be found' do - expect do - subject.perform(project.id, -1) + subject.perform(-1) end.not_to raise_error end end diff --git a/spec/workers/pages_domain_removal_cron_worker_spec.rb b/spec/workers/pages_domain_removal_cron_worker_spec.rb index 0e1171e8491..2408ad54189 100644 --- a/spec/workers/pages_domain_removal_cron_worker_spec.rb +++ b/spec/workers/pages_domain_removal_cron_worker_spec.rb @@ -9,25 +9,10 @@ describe PagesDomainRemovalCronWorker do context 'when there is domain which should be removed' do let!(:domain_for_removal) { create(:pages_domain, :should_be_removed) } - before do - stub_feature_flags(remove_disabled_domains: true) - end - it 'removes domain' do expect { worker.perform }.to change { PagesDomain.count }.by(-1) expect(PagesDomain.exists?).to eq(false) end - - context 'when domain removal is disabled' do - before do - stub_feature_flags(remove_disabled_domains: false) - end - - it 'does not remove pages domain' do - expect { worker.perform }.not_to change { PagesDomain.count } - expect(PagesDomain.find_by(domain: domain_for_removal.domain)).to be_present - end - end end context 'where there is a domain which scheduled for removal in the future' do diff --git a/yarn.lock b/yarn.lock index 07174e871e5..898159975fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,7 +9,7 @@ dependencies: "@babel/highlight" "^7.0.0" -"@babel/core@>=7.1.0", "@babel/core@^7.1.0", "@babel/core@^7.2.2": +"@babel/core@>=7.1.0", "@babel/core@^7.1.0": version "7.3.4" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.3.4.tgz#921a5a13746c21e32445bf0798680e9d11a6530b" integrity sha512-jRsuseXBo9pN197KnDwhhaaBzyZr2oIcLHHTt2oDdQrej5Qp57dCCJafWx5ivU8/alEYDpssYqv1MUqcxwQlrA== @@ -29,7 +29,27 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.0.0", "@babel/generator@^7.3.4": +"@babel/core@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.4.4.tgz#84055750b05fcd50f9915a826b44fa347a825250" + integrity sha512-lQgGX3FPRgbz2SKmhMtYgJvVzGZrmjaF4apZ2bLwofAKiSjxU0drPh4S/VasyYXwaTs+A1gvQ45BN8SQJzHsQQ== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/generator" "^7.4.4" + "@babel/helpers" "^7.4.4" + "@babel/parser" "^7.4.4" + "@babel/template" "^7.4.4" + "@babel/traverse" "^7.4.4" + "@babel/types" "^7.4.4" + convert-source-map "^1.1.0" + debug "^4.1.0" + json5 "^2.1.0" + lodash "^4.17.11" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + +"@babel/generator@^7.0.0": version "7.3.4" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.3.4.tgz#9aa48c1989257877a9d971296e5b73bfe72e446e" integrity sha512-8EXhHRFqlVVWXPezBW5keTiQi/rJMQTg/Y9uVCEZ0CAF3PKtCCaVRnp64Ii1ujhkoDhhF1fVsImoN4yJ2uz4Wg== @@ -40,6 +60,17 @@ source-map "^0.5.0" trim-right "^1.0.1" +"@babel/generator@^7.3.4", "@babel/generator@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.4.4.tgz#174a215eb843fc392c7edcaabeaa873de6e8f041" + integrity sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ== + dependencies: + "@babel/types" "^7.4.4" + jsesc "^2.5.1" + lodash "^4.17.11" + source-map "^0.5.0" + trim-right "^1.0.1" + "@babel/helper-annotate-as-pure@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz#323d39dd0b50e10c7c06ca7d7638e6864d8c5c32" @@ -55,34 +86,35 @@ "@babel/helper-explode-assignable-expression" "^7.1.0" "@babel/types" "^7.0.0" -"@babel/helper-call-delegate@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/helper-call-delegate/-/helper-call-delegate-7.1.0.tgz#6a957f105f37755e8645343d3038a22e1449cc4a" - integrity sha512-YEtYZrw3GUK6emQHKthltKNZwszBcHK58Ygcis+gVUrF4/FmTVr5CCqQNSfmvg2y+YDEANyYoaLz/SHsnusCwQ== +"@babel/helper-call-delegate@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-call-delegate/-/helper-call-delegate-7.4.4.tgz#87c1f8ca19ad552a736a7a27b1c1fcf8b1ff1f43" + integrity sha512-l79boDFJ8S1c5hvQvG+rc+wHw6IuH7YldmRKsYtpbawsxURu/paVy57FZMomGK22/JckepaikOkY0MoAmdyOlQ== dependencies: - "@babel/helper-hoist-variables" "^7.0.0" - "@babel/traverse" "^7.1.0" - "@babel/types" "^7.0.0" + "@babel/helper-hoist-variables" "^7.4.4" + "@babel/traverse" "^7.4.4" + "@babel/types" "^7.4.4" -"@babel/helper-create-class-features-plugin@^7.3.0": - version "7.3.0" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.3.0.tgz#2b01a81b3adc2b1287f9ee193688ef8dc71e718f" - integrity sha512-DUsQNS2CGLZZ7I3W3fvh0YpPDd6BuWJlDl+qmZZpABZHza2ErE3LxtEzLJFHFC1ZwtlAXvHhbFYbtM5o5B0WBw== +"@babel/helper-create-class-features-plugin@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.4.4.tgz#fc3d690af6554cc9efc607364a82d48f58736dba" + integrity sha512-UbBHIa2qeAGgyiNR9RszVF7bUHEdgS4JAUNT8SiqrAN6YJVxlOxeLr5pBzb5kan302dejJ9nla4RyKcR1XT6XA== dependencies: "@babel/helper-function-name" "^7.1.0" "@babel/helper-member-expression-to-functions" "^7.0.0" "@babel/helper-optimise-call-expression" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-replace-supers" "^7.2.3" + "@babel/helper-replace-supers" "^7.4.4" + "@babel/helper-split-export-declaration" "^7.4.4" -"@babel/helper-define-map@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.1.0.tgz#3b74caec329b3c80c116290887c0dd9ae468c20c" - integrity sha512-yPPcW8dc3gZLN+U1mhYV91QU3n5uTbx7DUdf8NnPbjS0RMwBuHi9Xt2MUgppmNz7CJxTBWsGczTiEp1CSOTPRg== +"@babel/helper-define-map@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.4.4.tgz#6969d1f570b46bdc900d1eba8e5d59c48ba2c12a" + integrity sha512-IX3Ln8gLhZpSuqHJSnTNBWGDE9kdkTEWl21A/K7PQ00tseBwbqCHTvNLHSBd9M0R5rER4h5Rsvj9vw0R5SieBg== dependencies: "@babel/helper-function-name" "^7.1.0" - "@babel/types" "^7.0.0" - lodash "^4.17.10" + "@babel/types" "^7.4.4" + lodash "^4.17.11" "@babel/helper-explode-assignable-expression@^7.1.0": version "7.1.0" @@ -108,12 +140,12 @@ dependencies: "@babel/types" "^7.0.0" -"@babel/helper-hoist-variables@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.0.0.tgz#46adc4c5e758645ae7a45deb92bab0918c23bb88" - integrity sha512-Ggv5sldXUeSKsuzLkddtyhyHe2YantsxWKNi7A+7LeD12ExRDWTRk29JCXpaHPAbMaIPZSil7n+lq78WY2VY7w== +"@babel/helper-hoist-variables@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.4.4.tgz#0298b5f25c8c09c53102d52ac4a98f773eb2850a" + integrity sha512-VYk2/H/BnYbZDDg39hr3t2kKyifAm1W6zHRfhx8jGjIHpQEBv9dry7oQ2f3+J703TLu69nYdxsovl0XYfcnK4w== dependencies: - "@babel/types" "^7.0.0" + "@babel/types" "^7.4.4" "@babel/helper-member-expression-to-functions@^7.0.0": version "7.0.0" @@ -141,6 +173,18 @@ "@babel/types" "^7.0.0" lodash "^4.17.10" +"@babel/helper-module-transforms@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.4.4.tgz#96115ea42a2f139e619e98ed46df6019b94414b8" + integrity sha512-3Z1yp8TVQf+B4ynN7WoHPKS8EkdTbgAEy0nU0rs/1Kw4pDgmvYH3rz3aI11KgxKCba2cn7N+tqzV1mY2HMN96w== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@babel/helper-simple-access" "^7.1.0" + "@babel/helper-split-export-declaration" "^7.4.4" + "@babel/template" "^7.4.4" + "@babel/types" "^7.4.4" + lodash "^4.17.11" + "@babel/helper-optimise-call-expression@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0.tgz#a2920c5702b073c15de51106200aa8cad20497d5" @@ -160,6 +204,13 @@ dependencies: lodash "^4.17.10" +"@babel/helper-regex@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.4.4.tgz#a47e02bc91fb259d2e6727c2a30013e3ac13c4a2" + integrity sha512-Y5nuB/kESmR3tKjU8Nkn1wMGEx1tjJX076HBMeL3XLQCu6vA/YRzuTW0bbb+qRnXvQGn+d6Rx953yffl8vEy7Q== + dependencies: + lodash "^4.17.11" + "@babel/helper-remap-async-to-generator@^7.1.0": version "7.1.0" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz#361d80821b6f38da75bd3f0785ece20a88c5fe7f" @@ -171,7 +222,7 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.0.0" -"@babel/helper-replace-supers@^7.1.0", "@babel/helper-replace-supers@^7.2.3": +"@babel/helper-replace-supers@^7.1.0": version "7.2.3" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.2.3.tgz#19970020cf22677d62b3a689561dbd9644d8c5e5" integrity sha512-GyieIznGUfPXPWu0yLS6U55Mz67AZD9cUk0BfirOWlPrXlBcan9Gz+vHGz+cPfuoweZSnPzPIm67VtQM0OWZbA== @@ -181,6 +232,16 @@ "@babel/traverse" "^7.2.3" "@babel/types" "^7.0.0" +"@babel/helper-replace-supers@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.4.4.tgz#aee41783ebe4f2d3ab3ae775e1cc6f1a90cefa27" + integrity sha512-04xGEnd+s01nY1l15EuMS1rfKktNF+1CkKmHoErDppjAAZL+IUBZpzT748x262HF7fibaQPhbvWUl5HeSt1EXg== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.0.0" + "@babel/helper-optimise-call-expression" "^7.0.0" + "@babel/traverse" "^7.4.4" + "@babel/types" "^7.4.4" + "@babel/helper-simple-access@^7.1.0": version "7.1.0" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz#65eeb954c8c245beaa4e859da6188f39d71e585c" @@ -189,12 +250,12 @@ "@babel/template" "^7.1.0" "@babel/types" "^7.0.0" -"@babel/helper-split-export-declaration@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz#3aae285c0311c2ab095d997b8c9a94cad547d813" - integrity sha512-MXkOJqva62dfC0w85mEf/LucPPS/1+04nmmRMPEBUB++hiiThQ2zPtX/mEWQ3mtzCEjIJvPY8nuwxXtQeQwUag== +"@babel/helper-split-export-declaration@^7.0.0", "@babel/helper-split-export-declaration@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz#ff94894a340be78f53f06af038b205c49d993677" + integrity sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q== dependencies: - "@babel/types" "^7.0.0" + "@babel/types" "^7.4.4" "@babel/helper-wrap-function@^7.1.0": version "7.1.0" @@ -206,14 +267,14 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.0.0" -"@babel/helpers@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.2.0.tgz#8335f3140f3144270dc63c4732a4f8b0a50b7a21" - integrity sha512-Fr07N+ea0dMcMN8nFpuK6dUIT7/ivt9yKQdEEnjVS83tG2pHwPi03gYmk/tyuwONnZ+sY+GFFPlWGgCtW1hF9A== +"@babel/helpers@^7.2.0", "@babel/helpers@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.4.4.tgz#868b0ef59c1dd4e78744562d5ce1b59c89f2f2a5" + integrity sha512-igczbR/0SeuPR8RFfC7tGrbdTbFL3QTvH6D+Z6zNxnTe//GyqmtHmDkzrqDmyZ3eSwPqB/LhyKoU5DXsp+Vp2A== dependencies: - "@babel/template" "^7.1.2" - "@babel/traverse" "^7.1.5" - "@babel/types" "^7.2.0" + "@babel/template" "^7.4.4" + "@babel/traverse" "^7.4.4" + "@babel/types" "^7.4.4" "@babel/highlight@^7.0.0": version "7.0.0" @@ -224,11 +285,16 @@ esutils "^2.0.2" js-tokens "^4.0.0" -"@babel/parser@^7.0.0", "@babel/parser@^7.2.2", "@babel/parser@^7.3.4": +"@babel/parser@^7.0.0": version "7.3.4" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.4.tgz#a43357e4bbf4b92a437fb9e465c192848287f27c" integrity sha512-tXZCqWtlOOP4wgCp6RjRvLmfuhnqTLy9VHwRochJBCP2nDm27JnnuFEnXFASVyQNHk36jD1tAammsCEEqgscIQ== +"@babel/parser@^7.2.2", "@babel/parser@^7.3.4", "@babel/parser@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.4.tgz#5977129431b8fe33471730d255ce8654ae1250b6" + integrity sha512-5pCS4mOsL+ANsFZGdvNLybx4wtqAZJ0MJjMHxvzI3bvIsz6sQvzW8XX92EYIkiPtIvcfG3Aj+Ir5VNyjnZhP7w== + "@babel/plugin-proposal-async-generator-functions@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz#b289b306669dce4ad20b0252889a15768c9d417e" @@ -238,12 +304,12 @@ "@babel/helper-remap-async-to-generator" "^7.1.0" "@babel/plugin-syntax-async-generators" "^7.2.0" -"@babel/plugin-proposal-class-properties@^7.3.0": - version "7.3.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.3.0.tgz#272636bc0fa19a0bc46e601ec78136a173ea36cd" - integrity sha512-wNHxLkEKTQ2ay0tnsam2z7fGZUi+05ziDJflEt3AZTP3oXLKHJp9HqhfroB/vdMvt3sda9fAbq7FsG8QPDrZBg== +"@babel/plugin-proposal-class-properties@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.4.4.tgz#93a6486eed86d53452ab9bab35e368e9461198ce" + integrity sha512-WjKTI8g8d5w1Bc9zgwSz2nfrsNQsXcCf9J9cdCvrJV6RF56yztwm4TmJC0MgJ9tvwO9gUA/mcYe89bLdGfiXFg== dependencies: - "@babel/helper-create-class-features-plugin" "^7.3.0" + "@babel/helper-create-class-features-plugin" "^7.4.4" "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-proposal-json-strings@^7.2.0": @@ -254,10 +320,10 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-json-strings" "^7.2.0" -"@babel/plugin-proposal-object-rest-spread@^7.3.1": - version "7.3.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.1.tgz#f69fb6a1ea6a4e1c503994a91d9cf76f3c4b36e8" - integrity sha512-Nmmv1+3LqxJu/V5jU9vJmxR/KIRWFk2qLHmbB56yRRRFhlaSuOVXscX3gUmhaKgUhzA3otOHVubbIEVYsZ0eZg== +"@babel/plugin-proposal-object-rest-spread@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.4.4.tgz#1ef173fcf24b3e2df92a678f027673b55e7e3005" + integrity sha512-dMBG6cSPBbHeEBdFXeQ2QLc5gUpg4Vkaz8octD4aoW/ISO+jBOcsuxYL7bsb5WSu8RLP6boxrBIALEHgoHtO9g== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-object-rest-spread" "^7.2.0" @@ -270,22 +336,22 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-optional-catch-binding" "^7.2.0" -"@babel/plugin-proposal-private-methods@^7.3.0": - version "7.3.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.3.0.tgz#da373257a66525cb76544c37ab2ce4c611568841" - integrity sha512-j6luy/F0MX6kd71e9hz97my2tBXTa+czAz+sscJVCRmjB9e9g2D4JN+tyfcwMCXUM2afj/tYCjzNaxwWJ4SdYg== +"@babel/plugin-proposal-private-methods@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.4.4.tgz#307b7db29d8ae2d259e7c0e6e665b1922d7ac856" + integrity sha512-EIV4fDVP3XwdizJ/H6308Km+d8xdLAUCAvsY8mjxhat9I3vNgssGhZuhgn/jw7IK5/91sN8PHtVGxMjeTSrSng== dependencies: - "@babel/helper-create-class-features-plugin" "^7.3.0" + "@babel/helper-create-class-features-plugin" "^7.4.4" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-proposal-unicode-property-regex@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.2.0.tgz#abe7281fe46c95ddc143a65e5358647792039520" - integrity sha512-LvRVYb7kikuOtIoUeWTkOxQEV1kYvL5B6U3iWEGCzPNRus1MzJweFqORTj+0jkxozkTSYNJozPOddxmqdqsRpw== +"@babel/plugin-proposal-unicode-property-regex@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.4.4.tgz#501ffd9826c0b91da22690720722ac7cb1ca9c78" + integrity sha512-j1NwnOqMG9mFUOH58JTFsA/+ZYzQLUZ/drqWUqxCYLGeu2JFZL8YrNC9hBxKmWtAuOCHPcRpgv7fhap09Fb4kA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-regex" "^7.0.0" - regexpu-core "^4.2.0" + "@babel/helper-regex" "^7.4.4" + regexpu-core "^4.5.4" "@babel/plugin-syntax-async-generators@^7.2.0": version "7.2.0" @@ -336,10 +402,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-async-to-generator@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.2.0.tgz#68b8a438663e88519e65b776f8938f3445b1a2ff" - integrity sha512-CEHzg4g5UraReozI9D4fblBYABs7IM6UerAVG7EJVrTLC5keh00aEuLUT+O40+mJCEzaXkYfTCUKIyeDfMOFFQ== +"@babel/plugin-transform-async-to-generator@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.4.4.tgz#a3f1d01f2f21cadab20b33a82133116f14fb5894" + integrity sha512-YiqW2Li8TXmzgbXw+STsSqPBPFnGviiaSp6CYOq55X8GQ2SGVLrXB6pNid8HkqkZAzOH6knbai3snhP7v0fNwA== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" @@ -352,26 +418,26 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-block-scoping@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.2.0.tgz#f17c49d91eedbcdf5dd50597d16f5f2f770132d4" - integrity sha512-vDTgf19ZEV6mx35yiPJe4fS02mPQUUcBNwWQSZFXSzTSbsJFQvHt7DqyS3LK8oOWALFOsJ+8bbqBgkirZteD5Q== +"@babel/plugin-transform-block-scoping@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.4.4.tgz#c13279fabf6b916661531841a23c4b7dae29646d" + integrity sha512-jkTUyWZcTrwxu5DD4rWz6rDB5Cjdmgz6z7M7RLXOJyCUkFBawssDGcGh8M/0FTSB87avyJI1HsTwUXp9nKA1PA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - lodash "^4.17.10" + lodash "^4.17.11" -"@babel/plugin-transform-classes@^7.2.0": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.2.2.tgz#6c90542f210ee975aa2aa8c8b5af7fa73a126953" - integrity sha512-gEZvgTy1VtcDOaQty1l10T3jQmJKlNVxLDCs+3rCVPr6nMkODLELxViq5X9l+rfxbie3XrfrMCYYY6eX3aOcOQ== +"@babel/plugin-transform-classes@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.4.4.tgz#0ce4094cdafd709721076d3b9c38ad31ca715eb6" + integrity sha512-/e44eFLImEGIpL9qPxSRat13I5QNRgBLu2hOQJCF7VLy/otSM/sypV1+XaIw5+502RX/+6YaSAPmldk+nhHDPw== dependencies: "@babel/helper-annotate-as-pure" "^7.0.0" - "@babel/helper-define-map" "^7.1.0" + "@babel/helper-define-map" "^7.4.4" "@babel/helper-function-name" "^7.1.0" "@babel/helper-optimise-call-expression" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-replace-supers" "^7.1.0" - "@babel/helper-split-export-declaration" "^7.0.0" + "@babel/helper-replace-supers" "^7.4.4" + "@babel/helper-split-export-declaration" "^7.4.4" globals "^11.1.0" "@babel/plugin-transform-computed-properties@^7.2.0": @@ -381,21 +447,21 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-destructuring@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.2.0.tgz#e75269b4b7889ec3a332cd0d0c8cff8fed0dc6f3" - integrity sha512-coVO2Ayv7g0qdDbrNiadE4bU7lvCd9H539m2gMknyVjjMdwF/iCOM7R+E8PkntoqLkltO0rk+3axhpp/0v68VQ== +"@babel/plugin-transform-destructuring@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.4.4.tgz#9d964717829cc9e4b601fc82a26a71a4d8faf20f" + integrity sha512-/aOx+nW0w8eHiEHm+BTERB2oJn5D127iye/SUQl7NjHy0lf+j7h4MKMMSOwdazGq9OxgiNADncE+SRJkCxjZpQ== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-dotall-regex@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.2.0.tgz#f0aabb93d120a8ac61e925ea0ba440812dbe0e49" - integrity sha512-sKxnyHfizweTgKZf7XsXu/CNupKhzijptfTM+bozonIuyVrLWVUvYjE2bhuSBML8VQeMxq4Mm63Q9qvcvUcciQ== +"@babel/plugin-transform-dotall-regex@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.4.4.tgz#361a148bc951444312c69446d76ed1ea8e4450c3" + integrity sha512-P05YEhRc2h53lZDjRPk/OektxCVevFzZs2Gfjd545Wde3k+yFDbXORgl2e0xpbq8mLcKJ7Idss4fAg0zORN/zg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-regex" "^7.0.0" - regexpu-core "^4.1.3" + "@babel/helper-regex" "^7.4.4" + regexpu-core "^4.5.4" "@babel/plugin-transform-duplicate-keys@^7.2.0": version "7.2.0" @@ -412,17 +478,17 @@ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-for-of@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.2.0.tgz#ab7468befa80f764bb03d3cb5eef8cc998e1cad9" - integrity sha512-Kz7Mt0SsV2tQk6jG5bBv5phVbkd0gd27SgYD4hH1aLMJRchM0dzHaXvrWhVZ+WxAlDoAKZ7Uy3jVTW2mKXQ1WQ== +"@babel/plugin-transform-for-of@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.4.4.tgz#0267fc735e24c808ba173866c6c4d1440fc3c556" + integrity sha512-9T/5Dlr14Z9TIEXLXkt8T1DU7F24cbhwhMNUziN3hB1AXoZcdzPcTiKGRn/6iOymDqtTKWnr/BtRKN9JwbKtdQ== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-function-name@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.2.0.tgz#f7930362829ff99a3174c39f0afcc024ef59731a" - integrity sha512-kWgksow9lHdvBC2Z4mxTsvc7YdY7w/V6B2vy9cTIPtLEE9NhwoWivaxdNM/S37elu5bqlLP/qOY906LukO9lkQ== +"@babel/plugin-transform-function-name@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.4.4.tgz#e1436116abb0610c2259094848754ac5230922ad" + integrity sha512-iU9pv7U+2jC9ANQkKeNF6DrPy4GBa4NWQtl6dHB4Pb3izX2JOEvDTFarlNsBj/63ZEzNNIAMs3Qw4fNCcSOXJA== dependencies: "@babel/helper-function-name" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" @@ -434,6 +500,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-member-expression-literals@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.2.0.tgz#fa10aa5c58a2cb6afcf2c9ffa8cb4d8b3d489a2d" + integrity sha512-HiU3zKkSU6scTidmnFJ0bMX8hz5ixC93b4MHMiYebmk2lUVNGOboPsqQvx5LzooihijUoLR/v7Nc1rbBtnc7FA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-modules-amd@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.2.0.tgz#82a9bce45b95441f617a24011dc89d12da7f4ee6" @@ -451,12 +524,21 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-simple-access" "^7.1.0" -"@babel/plugin-transform-modules-systemjs@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.2.0.tgz#912bfe9e5ff982924c81d0937c92d24994bb9068" - integrity sha512-aYJwpAhoK9a+1+O625WIjvMY11wkB/ok0WClVwmeo3mCjcNRjt+/8gHWrB5i+00mUju0gWsBkQnPpdvQ7PImmQ== +"@babel/plugin-transform-modules-commonjs@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.4.4.tgz#0bef4713d30f1d78c2e59b3d6db40e60192cac1e" + integrity sha512-4sfBOJt58sEo9a2BQXnZq+Q3ZTSAUXyK3E30o36BOGnJ+tvJ6YSxF0PG6kERvbeISgProodWuI9UVG3/FMY6iw== + dependencies: + "@babel/helper-module-transforms" "^7.4.4" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-simple-access" "^7.1.0" + +"@babel/plugin-transform-modules-systemjs@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.4.4.tgz#dc83c5665b07d6c2a7b224c00ac63659ea36a405" + integrity sha512-MSiModfILQc3/oqnG7NrP1jHaSPryO6tA2kOMmAQApz5dayPxWiHqmq4sWH2xF5LcQK56LlbKByCd8Aah/OIkQ== dependencies: - "@babel/helper-hoist-variables" "^7.0.0" + "@babel/helper-hoist-variables" "^7.4.4" "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-transform-modules-umd@^7.2.0": @@ -467,17 +549,17 @@ "@babel/helper-module-transforms" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-named-capturing-groups-regex@^7.3.0": - version "7.3.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.3.0.tgz#140b52985b2d6ef0cb092ef3b29502b990f9cd50" - integrity sha512-NxIoNVhk9ZxS+9lSoAQ/LM0V2UEvARLttEHUrRDGKFaAxOYQcrkN/nLRE+BbbicCAvZPl7wMP0X60HsHE5DtQw== +"@babel/plugin-transform-named-capturing-groups-regex@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.4.4.tgz#5611d96d987dfc4a3a81c4383bb173361037d68d" + integrity sha512-Ki+Y9nXBlKfhD+LXaRS7v95TtTGYRAf9Y1rTDiE75zf8YQz4GDaWRXosMfJBXxnk88mGFjWdCRIeqDbon7spYA== dependencies: regexp-tree "^0.1.0" -"@babel/plugin-transform-new-target@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.0.0.tgz#ae8fbd89517fa7892d20e6564e641e8770c3aa4a" - integrity sha512-yin069FYjah+LbqfGeTfzIBODex/e++Yfa0rH0fpfam9uTbuEeEOx5GLGr210ggOV77mVRNoeqSYqeuaqSzVSw== +"@babel/plugin-transform-new-target@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.4.4.tgz#18d120438b0cc9ee95a47f2c72bc9768fbed60a5" + integrity sha512-r1z3T2DNGQwwe2vPGZMBNjioT2scgWzK9BCnDEh+46z8EEwXBq24uRzd65I7pjtugzPSj921aM15RpESgzsSuA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" @@ -489,21 +571,35 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-replace-supers" "^7.1.0" -"@babel/plugin-transform-parameters@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.2.0.tgz#0d5ad15dc805e2ea866df4dd6682bfe76d1408c2" - integrity sha512-kB9+hhUidIgUoBQ0MsxMewhzr8i60nMa2KgeJKQWYrqQpqcBYtnpR+JgkadZVZoaEZ/eKu9mclFaVwhRpLNSzA== +"@babel/plugin-transform-parameters@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.4.4.tgz#7556cf03f318bd2719fe4c922d2d808be5571e16" + integrity sha512-oMh5DUO1V63nZcu/ZVLQFqiihBGo4OpxJxR1otF50GMeCLiRx5nUdtokd+u9SuVJrvvuIh9OosRFPP4pIPnwmw== dependencies: - "@babel/helper-call-delegate" "^7.1.0" + "@babel/helper-call-delegate" "^7.4.4" "@babel/helper-get-function-arity" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-regenerator@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.0.0.tgz#5b41686b4ed40bef874d7ed6a84bdd849c13e0c1" - integrity sha512-sj2qzsEx8KDVv1QuJc/dEfilkg3RRPvPYx/VnKLtItVQRWt1Wqf5eVCOLZm29CiGFfYYsA3VPjfizTCV0S0Dlw== +"@babel/plugin-transform-property-literals@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.2.0.tgz#03e33f653f5b25c4eb572c98b9485055b389e905" + integrity sha512-9q7Dbk4RhgcLp8ebduOpCbtjh7C0itoLYHXd9ueASKAG/is5PQtMR5VJGka9NKqGhYEGn5ITahd4h9QeBMylWQ== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-regenerator@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.4.4.tgz#5b4da4df79391895fca9e28f99e87e22cfc02072" + integrity sha512-Zz3w+pX1SI0KMIiqshFZkwnVGUhDZzpX2vtPzfJBKQQq8WsP/Xy9DNdELWivxcKOCX/Pywge4SiEaPaLtoDT4g== dependencies: - regenerator-transform "^0.13.3" + regenerator-transform "^0.13.4" + +"@babel/plugin-transform-reserved-words@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.2.0.tgz#4792af87c998a49367597d07fedf02636d2e1634" + integrity sha512-fz43fqW8E1tAB3DKF19/vxbpib1fuyCwSPE418ge5ZxILnBhWyhtPgz8eh1RCGGJlwvksHkyxMxh0eenFi+kFw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-transform-shorthand-properties@^7.2.0": version "7.2.0" @@ -527,10 +623,10 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-regex" "^7.0.0" -"@babel/plugin-transform-template-literals@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.2.0.tgz#d87ed01b8eaac7a92473f608c97c089de2ba1e5b" - integrity sha512-FkPix00J9A/XWXv4VoKJBMeSkyY9x/TqIh76wzcdfl57RJJcf8CehQ08uwfhCDNtRQYtHQKBTwKZDEyjE13Lwg== +"@babel/plugin-transform-template-literals@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.4.4.tgz#9d28fea7bbce637fb7612a0750989d8321d4bcb0" + integrity sha512-mQrEC4TWkhLN0z8ygIvEL9ZEToPhG5K7KDW3pzGqOfIGZ28Jb0POUkeWcoz8HnHvhFy6dwAT1j8OzqN8s804+g== dependencies: "@babel/helper-annotate-as-pure" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" @@ -542,70 +638,75 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-unicode-regex@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.2.0.tgz#4eb8db16f972f8abb5062c161b8b115546ade08b" - integrity sha512-m48Y0lMhrbXEJnVUaYly29jRXbQ3ksxPrS1Tg8t+MHqzXhtBYAvI51euOBaoAlZLPHsieY9XPVMf80a5x0cPcA== +"@babel/plugin-transform-unicode-regex@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.4.4.tgz#ab4634bb4f14d36728bf5978322b35587787970f" + integrity sha512-il+/XdNw01i93+M9J9u4T7/e/Ue/vWfNZE4IRUQjplu2Mqb/AFTDimkw2tdEdSH50wuQXZAbXSql0UphQke+vA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-regex" "^7.0.0" - regexpu-core "^4.1.3" + "@babel/helper-regex" "^7.4.4" + regexpu-core "^4.5.4" -"@babel/preset-env@^7.3.1": - version "7.3.1" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.3.1.tgz#389e8ca6b17ae67aaf9a2111665030be923515db" - integrity sha512-FHKrD6Dxf30e8xgHQO0zJZpUPfVZg+Xwgz5/RdSWCbza9QLNk4Qbp40ctRoqDxml3O8RMzB1DU55SXeDG6PqHQ== +"@babel/preset-env@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.4.4.tgz#b6f6825bfb27b3e1394ca3de4f926482722c1d6f" + integrity sha512-FU1H+ACWqZZqfw1x2G1tgtSSYSfxJLkpaUQL37CenULFARDo+h4xJoVHzRoHbK+85ViLciuI7ME4WTIhFRBBlw== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-proposal-async-generator-functions" "^7.2.0" "@babel/plugin-proposal-json-strings" "^7.2.0" - "@babel/plugin-proposal-object-rest-spread" "^7.3.1" + "@babel/plugin-proposal-object-rest-spread" "^7.4.4" "@babel/plugin-proposal-optional-catch-binding" "^7.2.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.2.0" + "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" "@babel/plugin-syntax-async-generators" "^7.2.0" "@babel/plugin-syntax-json-strings" "^7.2.0" "@babel/plugin-syntax-object-rest-spread" "^7.2.0" "@babel/plugin-syntax-optional-catch-binding" "^7.2.0" "@babel/plugin-transform-arrow-functions" "^7.2.0" - "@babel/plugin-transform-async-to-generator" "^7.2.0" + "@babel/plugin-transform-async-to-generator" "^7.4.4" "@babel/plugin-transform-block-scoped-functions" "^7.2.0" - "@babel/plugin-transform-block-scoping" "^7.2.0" - "@babel/plugin-transform-classes" "^7.2.0" + "@babel/plugin-transform-block-scoping" "^7.4.4" + "@babel/plugin-transform-classes" "^7.4.4" "@babel/plugin-transform-computed-properties" "^7.2.0" - "@babel/plugin-transform-destructuring" "^7.2.0" - "@babel/plugin-transform-dotall-regex" "^7.2.0" + "@babel/plugin-transform-destructuring" "^7.4.4" + "@babel/plugin-transform-dotall-regex" "^7.4.4" "@babel/plugin-transform-duplicate-keys" "^7.2.0" "@babel/plugin-transform-exponentiation-operator" "^7.2.0" - "@babel/plugin-transform-for-of" "^7.2.0" - "@babel/plugin-transform-function-name" "^7.2.0" + "@babel/plugin-transform-for-of" "^7.4.4" + "@babel/plugin-transform-function-name" "^7.4.4" "@babel/plugin-transform-literals" "^7.2.0" + "@babel/plugin-transform-member-expression-literals" "^7.2.0" "@babel/plugin-transform-modules-amd" "^7.2.0" - "@babel/plugin-transform-modules-commonjs" "^7.2.0" - "@babel/plugin-transform-modules-systemjs" "^7.2.0" + "@babel/plugin-transform-modules-commonjs" "^7.4.4" + "@babel/plugin-transform-modules-systemjs" "^7.4.4" "@babel/plugin-transform-modules-umd" "^7.2.0" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.3.0" - "@babel/plugin-transform-new-target" "^7.0.0" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.4.4" + "@babel/plugin-transform-new-target" "^7.4.4" "@babel/plugin-transform-object-super" "^7.2.0" - "@babel/plugin-transform-parameters" "^7.2.0" - "@babel/plugin-transform-regenerator" "^7.0.0" + "@babel/plugin-transform-parameters" "^7.4.4" + "@babel/plugin-transform-property-literals" "^7.2.0" + "@babel/plugin-transform-regenerator" "^7.4.4" + "@babel/plugin-transform-reserved-words" "^7.2.0" "@babel/plugin-transform-shorthand-properties" "^7.2.0" "@babel/plugin-transform-spread" "^7.2.0" "@babel/plugin-transform-sticky-regex" "^7.2.0" - "@babel/plugin-transform-template-literals" "^7.2.0" + "@babel/plugin-transform-template-literals" "^7.4.4" "@babel/plugin-transform-typeof-symbol" "^7.2.0" - "@babel/plugin-transform-unicode-regex" "^7.2.0" - browserslist "^4.3.4" + "@babel/plugin-transform-unicode-regex" "^7.4.4" + "@babel/types" "^7.4.4" + browserslist "^4.5.2" + core-js-compat "^3.0.0" invariant "^2.2.2" js-levenshtein "^1.1.3" - semver "^5.3.0" + semver "^5.5.0" "@babel/standalone@^7.0.0": version "7.3.4" resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.3.4.tgz#b622c1e522acef91b2a14f22bdcdd4f935a1a474" integrity sha512-4L9c5i4WlGqbrjOVX0Yp8TIR5cEiw1/tPYYZENW/iuO2uI6viY38U7zALidzNfGdZIwNc+A/AWqMEWKeScWkBg== -"@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.1.2", "@babel/template@^7.2.2": +"@babel/template@^7.0.0": version "7.2.2" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.2.2.tgz#005b3fdf0ed96e88041330379e0da9a708eb2907" integrity sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g== @@ -614,7 +715,16 @@ "@babel/parser" "^7.2.2" "@babel/types" "^7.2.2" -"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.2.3", "@babel/traverse@^7.3.4": +"@babel/template@^7.1.0", "@babel/template@^7.2.2", "@babel/template@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237" + integrity sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/parser" "^7.4.4" + "@babel/types" "^7.4.4" + +"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.2.3": version "7.3.4" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.3.4.tgz#1330aab72234f8dea091b08c4f8b9d05c7119e06" integrity sha512-TvTHKp6471OYEcE/91uWmhR6PrrYywQntCHSaZ8CM8Vmp+pjAusal4nGB2WCCQd0rvI7nOMKn9GnbcvTUz3/ZQ== @@ -629,10 +739,25 @@ globals "^11.1.0" lodash "^4.17.11" -"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.3.4.tgz#bf482eaeaffb367a28abbf9357a94963235d90ed" - integrity sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ== +"@babel/traverse@^7.3.4", "@babel/traverse@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.4.4.tgz#0776f038f6d78361860b6823887d4f3937133fe8" + integrity sha512-Gw6qqkw/e6AGzlyj9KnkabJX7VcubqPtkUQVAwkc0wUMldr3A/hezNB3Rc5eIvId95iSGkGIOe5hh1kMKf951A== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/generator" "^7.4.4" + "@babel/helper-function-name" "^7.1.0" + "@babel/helper-split-export-declaration" "^7.4.4" + "@babel/parser" "^7.4.4" + "@babel/types" "^7.4.4" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.11" + +"@babel/types@^7.0.0", "@babel/types@^7.2.2", "@babel/types@^7.3.4", "@babel/types@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.4.4.tgz#8db9e9a629bb7c29370009b4b779ed93fe57d5f0" + integrity sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ== dependencies: esutils "^2.0.2" lodash "^4.17.11" @@ -1796,7 +1921,7 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" -browserslist@^4.3.4, browserslist@^4.4.1: +browserslist@^4.4.1: version "4.4.1" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.4.1.tgz#42e828954b6b29a7a53e352277be429478a69062" integrity sha512-pEBxEXg7JwaakBXjATYw/D1YZh4QUSCX/Mnd/wnqSRPPSi1U39iDhDoKGoBUcraKdxDlrYqJxSI5nNvD+dWP2A== @@ -1805,6 +1930,15 @@ browserslist@^4.3.4, browserslist@^4.4.1: electron-to-chromium "^1.3.103" node-releases "^1.1.3" +browserslist@^4.5.2, browserslist@^4.5.4: + version "4.6.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.6.0.tgz#5274028c26f4d933d5b1323307c1d1da5084c9ff" + integrity sha512-Jk0YFwXBuMOOol8n6FhgkDzn3mY9PYLYGk29zybF05SbRTsMgPqmTNeQQhOghCxq5oFqAXE3u4sYddr4C0uRhg== + dependencies: + caniuse-lite "^1.0.30000967" + electron-to-chromium "^1.3.133" + node-releases "^1.1.19" + bs-logger@0.x: version "0.2.6" resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" @@ -2000,6 +2134,11 @@ caniuse-lite@^1.0.30000929, caniuse-lite@^1.0.30000932: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000936.tgz#5d33b118763988bf721b9b8ad436d0400e4a116b" integrity sha512-orX4IdpbFhdNO7bTBhSbahp1EBpqzBc+qrvTRVUFfZgA4zta7TdM6PN5ZxkEUgDnz36m+PfWGcdX7AVfFWItJw== +caniuse-lite@^1.0.30000967: + version "1.0.30000969" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000969.tgz#7664f571f2072657bde70b00a1fc1ba41f1942a9" + integrity sha512-Kus0yxkoAJgVc0bax7S4gLSlFifCa7MnSZL9p9VuS/HIKEL4seaqh28KIQAAO50cD/rJ5CiJkJFapkdDAlhFxQ== + capture-exit@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-1.2.0.tgz#1c5fcc489fd0ab00d4f1ac7ae1072e3173fbab6f" @@ -2531,6 +2670,26 @@ copy-to-clipboard@^3.0.8: dependencies: toggle-selection "^1.0.3" +core-js-compat@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.0.1.tgz#bff73ba31ca8687431b9c88f78d3362646fb76f0" + integrity sha512-2pC3e+Ht/1/gD7Sim/sqzvRplMiRnFQVlPpDVaHtY9l7zZP7knamr3VRD6NyGfHd84MrDC0tAM9ulNxYMW0T3g== + dependencies: + browserslist "^4.5.4" + core-js "3.0.1" + core-js-pure "3.0.1" + semver "^6.0.0" + +core-js-pure@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.0.1.tgz#37358fb0d024e6b86d443d794f4e37e949098cbe" + integrity sha512-mSxeQ6IghKW3MoyF4cz19GJ1cMm7761ON+WObSyLfTu/Jn3x7w4NwNFnrZxgl4MTSvYYepVLNuRtlB4loMwJ5g== + +core-js@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.0.1.tgz#1343182634298f7f38622f95e73f54e48ddf4738" + integrity sha512-sco40rF+2KlE0ROMvydjkrVMMG1vYilP2ALoRXcYR4obqbYIuV3Bg+51GEDW+HF8n7NRA+iaA4qD0nD9lo9mew== + core-js@^2.2.0, core-js@^2.4.0, core-js@^2.4.1: version "2.5.7" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" @@ -3524,6 +3683,11 @@ electron-to-chromium@^1.3.103: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.113.tgz#b1ccf619df7295aea17bc6951dc689632629e4a9" integrity sha512-De+lPAxEcpxvqPTyZAXELNpRZXABRxf+uL/rSykstQhzj/B0l1150G/ExIIxKc16lI89Hgz81J0BHAcbTqK49g== +electron-to-chromium@^1.3.133: + version "1.3.135" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.135.tgz#f5799b95f2bcd8de17cde47d63392d83a4477041" + integrity sha512-xXLNstRdVsisPF3pL3H9TVZo2XkMILfqtD6RiWIUmDK2sFX1Bjwqmd8LBp0Kuo2FgKO63JXPoEVGm8WyYdwP0Q== + elliptic@^6.0.0: version "6.4.0" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" @@ -4524,12 +4688,12 @@ fs.realpath@^1.0.0: integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= fsevents@^1.2.2, fsevents@^1.2.3: - version "1.2.4" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426" - integrity sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg== + version "1.2.9" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.9.tgz#3f5ed66583ccd6f400b5a00db6f7e861363e388f" + integrity sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw== dependencies: - nan "^2.9.2" - node-pre-gyp "^0.10.0" + nan "^2.12.1" + node-pre-gyp "^0.12.0" fstream@^1.0.0, fstream@^1.0.2: version "1.0.11" @@ -4722,7 +4886,12 @@ global-prefix@^3.0.0: kind-of "^6.0.2" which "^1.3.1" -globals@^11.1.0, globals@^11.7.0: +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^11.7.0: version "11.7.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.7.0.tgz#a583faa43055b1aca771914bf68258e2fc125673" integrity sha512-K8BNSPySfeShBQXsahYB/AbbWruVOTyVpgoIDnl8odPpeSfP2J5QO2oLFFdl2j7GfDCtZj2bMKar2T49itTPCg== @@ -6374,9 +6543,9 @@ jsdom@^11.5.1: xml-name-validator "^3.0.0" jsesc@^2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.1.tgz#e421a2a8e20d6b0819df28908f782526b96dd1fe" - integrity sha1-5CGiqOINawgZ3yiQj3glJrlt0f4= + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== jsesc@~0.5.0: version "0.5.0" @@ -6731,17 +6900,12 @@ locate-path@^3.0.0: p-locate "^3.0.0" path-exists "^3.0.0" -lodash.assign@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" - integrity sha1-DZnzzNem0mHRm9rrkkUAXShYCOc= - lodash.camelcase@4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= -lodash.clonedeep@^4.3.2, lodash.clonedeep@^4.5.0: +lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= @@ -7310,10 +7474,15 @@ mute-stream@0.0.7: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= -nan@^2.10.0, nan@^2.9.2: - version "2.13.1" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.1.tgz#a15bee3790bde247e8f38f1d446edcdaeb05f2dd" - integrity sha512-I6YB/YEuDeUZMmhscXKxGgZlFnhsn5y0hgOZBadkzfTRrZBtJDZeg6eQf7PYMIEclwmorTKK8GztsyOUSVBREA== +nan@^2.12.1: + version "2.14.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" + integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== + +nan@^2.13.2: + version "2.13.2" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7" + integrity sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw== nanomatch@^1.2.9: version "1.2.9" @@ -7447,10 +7616,10 @@ node-notifier@^5.2.1: shellwords "^0.1.1" which "^1.3.0" -node-pre-gyp@^0.10.0: - version "0.10.3" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc" - integrity sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A== +node-pre-gyp@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz#39ba4bb1439da030295f899e3b520b7785766149" + integrity sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A== dependencies: detect-libc "^1.0.2" mkdirp "^0.5.1" @@ -7463,6 +7632,13 @@ node-pre-gyp@^0.10.0: semver "^5.3.0" tar "^4" +node-releases@^1.1.19: + version "1.1.19" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.19.tgz#c492d1e381fea0350b338b646c27867e88e91b3d" + integrity sha512-SH/B4WwovHbulIALsQllAVwqZZD1kPmKCqrhGfR29dXjLAVZMHvBjD3S6nL9D/J9QkmZ1R92/0wCMDKXUUvyyA== + dependencies: + semver "^5.3.0" + node-releases@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.3.tgz#aad9ce0dcb98129c753f772c0aa01360fb90fbd2" @@ -7470,10 +7646,10 @@ node-releases@^1.1.3: dependencies: semver "^5.3.0" -node-sass@^4.11.0: - version "4.11.0" - resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.11.0.tgz#183faec398e9cbe93ba43362e2768ca988a6369a" - integrity sha512-bHUdHTphgQJZaF1LASx0kAviPH7sGlcyNhWade4eVIpFp6tsn7SV8xNMTbsQFpEV9VXpnwTTnNYlfsZXgGgmkA== +node-sass@^4.12.0: + version "4.12.0" + resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.12.0.tgz#0914f531932380114a30cc5fa4fa63233a25f017" + integrity sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ== dependencies: async-foreach "^0.1.3" chalk "^1.1.1" @@ -7482,12 +7658,10 @@ node-sass@^4.11.0: get-stdin "^4.0.1" glob "^7.0.3" in-publish "^2.0.0" - lodash.assign "^4.2.0" - lodash.clonedeep "^4.3.2" - lodash.mergewith "^4.6.0" + lodash "^4.17.11" meow "^3.7.0" mkdirp "^0.5.1" - nan "^2.10.0" + nan "^2.13.2" node-gyp "^3.8.0" npmlog "^4.0.0" request "^2.88.0" @@ -8879,10 +9053,10 @@ redent@^2.0.0: indent-string "^3.0.0" strip-indent "^2.0.0" -regenerate-unicode-properties@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-7.0.0.tgz#107405afcc4a190ec5ed450ecaa00ed0cafa7a4c" - integrity sha512-s5NGghCE4itSlUS+0WUj88G6cfMVMmH8boTPNvABf8od+2dhT9WDlWu8n01raQAJZMOK8Ch6jSexaRO7swd6aw== +regenerate-unicode-properties@^8.0.2: + version "8.1.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz#ef51e0f0ea4ad424b77bf7cb41f3e015c70a3f0e" + integrity sha512-LGZzkgtLY79GeXLm8Dp0BVLdQlWICzBnJz/ipWUgo59qBaZ+BHtq51P2q1uVZlppMuUAT37SDk39qUbjTWB7bA== dependencies: regenerate "^1.4.0" @@ -8901,10 +9075,10 @@ regenerator-runtime@^0.11.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== -regenerator-transform@^0.13.3: - version "0.13.3" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.3.tgz#264bd9ff38a8ce24b06e0636496b2c856b57bcbb" - integrity sha512-5ipTrZFSq5vU2YoGoww4uaRVAK4wyYC4TSICibbfEPOruUu8FFP7ErV0BjmbIOEpn3O/k9na9UEdYR/3m7N6uA== +regenerator-transform@^0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.4.tgz#18f6763cf1382c69c36df76c6ce122cc694284fb" + integrity sha512-T0QMBjK3J0MtxjPmdIMXm72Wvj2Abb0Bd4HADdfijwMdoIsyQZ6fWC7kDFhk2YinBBEMZDL7Y7wh0J1sGx3S4A== dependencies: private "^0.1.6" @@ -8939,17 +9113,17 @@ regexpu-core@^1.0.0: regjsgen "^0.2.0" regjsparser "^0.1.4" -regexpu-core@^4.1.3, regexpu-core@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.2.0.tgz#a3744fa03806cffe146dea4421a3e73bdcc47b1d" - integrity sha512-Z835VSnJJ46CNBttalHD/dB+Sj2ezmY6Xp38npwU87peK6mqOzOpV8eYktdkLTEkzzD+JsTcxd84ozd8I14+rw== +regexpu-core@^4.5.4: + version "4.5.4" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.5.4.tgz#080d9d02289aa87fe1667a4f5136bc98a6aebaae" + integrity sha512-BtizvGtFQKGPUcTy56o3nk1bGRp4SZOTYrDtGNlqCQufptV5IkkLN6Emw+yunAJjzf+C9FQFtvq7IoA3+oMYHQ== dependencies: regenerate "^1.4.0" - regenerate-unicode-properties "^7.0.0" - regjsgen "^0.4.0" - regjsparser "^0.3.0" + regenerate-unicode-properties "^8.0.2" + regjsgen "^0.5.0" + regjsparser "^0.6.0" unicode-match-property-ecmascript "^1.0.4" - unicode-match-property-value-ecmascript "^1.0.2" + unicode-match-property-value-ecmascript "^1.1.0" registry-auth-token@^3.0.1: version "3.3.2" @@ -8971,10 +9145,10 @@ regjsgen@^0.2.0: resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" integrity sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc= -regjsgen@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.4.0.tgz#c1eb4c89a209263f8717c782591523913ede2561" - integrity sha512-X51Lte1gCYUdlwhF28+2YMO0U6WeN0GLpgpA7LK7mbdDnkQYiwvEpmpe0F/cv5L14EbxgrdayAG3JETBv0dbXA== +regjsgen@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.0.tgz#a7634dc08f89209c2049adda3525711fb97265dd" + integrity sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA== regjsparser@^0.1.4: version "0.1.5" @@ -8983,10 +9157,10 @@ regjsparser@^0.1.4: dependencies: jsesc "~0.5.0" -regjsparser@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.3.0.tgz#3c326da7fcfd69fa0d332575a41c8c0cdf588c96" - integrity sha512-zza72oZBBHzt64G7DxdqrOo/30bhHkwMUoT0WqfGu98XLd7N+1tsy5MJ96Bk4MD0y74n629RhmrGW6XlnLLwCA== +regjsparser@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.0.tgz#f1e6ae8b7da2bae96c99399b868cd6c933a2ba9c" + integrity sha512-RQ7YyokLiQBomUJuUG8iGVvkgOLxwyZM8k6d3q5SAXpg4r5TZJZigKFvC6PpD+qQ98bCDC5YelPeA3EucDoNeQ== dependencies: jsesc "~0.5.0" @@ -9184,13 +9358,20 @@ resolve@1.1.7, resolve@1.1.x: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@1.x, resolve@^1.10.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.9.0: +resolve@1.x, resolve@^1.10.0, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.9.0: version "1.10.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba" integrity sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg== dependencies: path-parse "^1.0.6" +resolve@^1.3.2: + version "1.11.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.11.0.tgz#4014870ba296176b86343d50b60f3b50609ce232" + integrity sha512-WL2pBDjqT6pGUNSUzMw00o4T7If+z4H2x3Gz893WoUQ5KW8Vr9txp00ykiP16VBaZF5+j/OcXJHZ9+PCvdiDKw== + dependencies: + path-parse "^1.0.6" + responselike@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" @@ -9402,11 +9583,21 @@ semver-diff@^2.0.0: dependencies: semver "^5.0.3" -"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0: +"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.5, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0: version "5.6.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== +semver@^5.4.1: + version "5.7.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" + integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA== + +semver@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.0.0.tgz#05e359ee571e5ad7ed641a6eec1e547ba52dea65" + integrity sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ== + semver@~5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" @@ -10675,10 +10866,10 @@ unicode-match-property-ecmascript@^1.0.4: unicode-canonical-property-names-ecmascript "^1.0.4" unicode-property-aliases-ecmascript "^1.0.4" -unicode-match-property-value-ecmascript@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.0.2.tgz#9f1dc76926d6ccf452310564fd834ace059663d4" - integrity sha512-Rx7yODZC1L/T8XKo/2kNzVAQaRE88AaMvI1EF/Xnj3GW2wzN6fop9DDWuFAKUVFH7vozkz26DzP0qyWLKLIVPQ== +unicode-match-property-value-ecmascript@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz#5b4b426e08d13a80365e0d657ac7a6c1ec46a277" + integrity sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g== unicode-property-aliases-ecmascript@^1.0.4: version "1.0.4" |